From 43f8f3638a9550a0d2d14d827ef3ee53f552f4e3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 27 Dec 2020 02:42:13 +0300 Subject: [PATCH 01/65] Fix mod using reference equality unless casted to `IMod` --- osu.Game/Online/API/APIMod.cs | 4 ++-- osu.Game/Rulesets/Mods/IMod.cs | 3 +-- osu.Game/Rulesets/Mods/Mod.cs | 4 ++-- osu.Game/Scoring/ScoreInfo.cs | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 780e5daa16..f4e0e1b11f 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Online.API { - public class APIMod : IMod + public class APIMod : IMod, IEquatable { [JsonProperty("acronym")] public string Acronym { get; set; } @@ -52,7 +52,7 @@ namespace osu.Game.Online.API return resultMod; } - public bool Equals(IMod other) => Acronym == other?.Acronym; + public bool Equals(APIMod other) => Acronym == other?.Acronym; public override string ToString() { diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index a5e19f293c..448ad0eb30 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.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 Newtonsoft.Json; namespace osu.Game.Rulesets.Mods { - public interface IMod : IEquatable + public interface IMod { /// /// The shortened name of this mod. diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b8dc7a2661..33550e070b 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods /// The base class for gameplay modifiers. /// [ExcludeFromDynamicCompile] - public abstract class Mod : IMod, IJsonSerializable + public abstract class Mod : IMod, IEquatable, IJsonSerializable { /// /// The name of this mod. @@ -149,6 +149,6 @@ namespace osu.Game.Rulesets.Mods return copy; } - public bool Equals(IMod other) => GetType() == other?.GetType(); + public bool Equals(Mod other) => GetType() == other?.GetType(); } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index f5192f3a40..59eaa994c2 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -252,11 +252,11 @@ namespace osu.Game.Scoring } [Serializable] - protected class DeserializedMod : IMod + protected class DeserializedMod : IMod, IEquatable { public string Acronym { get; set; } - public bool Equals(IMod other) => Acronym == other?.Acronym; + public bool Equals(DeserializedMod other) => Acronym == other?.Acronym; } public override string ToString() => $"{User} playing {Beatmap}"; From 5efcdbd431153a902fd510d727f9b02400afcb66 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 28 Dec 2020 15:19:28 +0300 Subject: [PATCH 02/65] Fix IMod now using reference equality as well --- osu.Game/Online/API/APIMod.cs | 3 ++- osu.Game/Rulesets/Mods/IMod.cs | 3 ++- osu.Game/Rulesets/Mods/Mod.cs | 3 ++- osu.Game/Scoring/ScoreInfo.cs | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index f4e0e1b11f..c1b243c743 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -52,7 +52,8 @@ namespace osu.Game.Online.API return resultMod; } - public bool Equals(APIMod other) => Acronym == other?.Acronym; + public bool Equals(IMod other) => other is APIMod them && Equals(them); + public bool Equals(APIMod other) => ((IMod)this).Equals(other); public override string ToString() { diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index 448ad0eb30..a5e19f293c 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.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 Newtonsoft.Json; namespace osu.Game.Rulesets.Mods { - public interface IMod + public interface IMod : IEquatable { /// /// The shortened name of this mod. diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 33550e070b..3ccebe4174 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -149,6 +149,7 @@ namespace osu.Game.Rulesets.Mods return copy; } - public bool Equals(Mod other) => GetType() == other?.GetType(); + public bool Equals(IMod other) => other is Mod them && Equals(them); + public bool Equals(Mod other) => Acronym == other?.Acronym; } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 59eaa994c2..335671ea4e 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -256,7 +256,8 @@ namespace osu.Game.Scoring { public string Acronym { get; set; } - public bool Equals(DeserializedMod other) => Acronym == other?.Acronym; + bool IEquatable.Equals(IMod other) => other is DeserializedMod them && Equals(them); + public bool Equals(DeserializedMod other) => ((IMod)this).Equals(other); } public override string ToString() => $"{User} playing {Beatmap}"; From 41b79d938b401d7101440e4ee21f6f28a7590b4a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 28 Dec 2020 15:30:52 +0300 Subject: [PATCH 03/65] Fix wrong checks.. --- osu.Game/Online/API/APIMod.cs | 2 +- osu.Game/Rulesets/Mods/Mod.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index c1b243c743..f4fed4e5c5 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online.API } public bool Equals(IMod other) => other is APIMod them && Equals(them); - public bool Equals(APIMod other) => ((IMod)this).Equals(other); + public bool Equals(APIMod other) => Acronym == other?.Acronym; public override string ToString() { diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 3ccebe4174..dbb2a0fdc1 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -150,6 +150,6 @@ namespace osu.Game.Rulesets.Mods } public bool Equals(IMod other) => other is Mod them && Equals(them); - public bool Equals(Mod other) => Acronym == other?.Acronym; + public bool Equals(Mod other) => GetType() == other?.GetType(); } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 335671ea4e..1e5742c358 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -257,7 +257,7 @@ namespace osu.Game.Scoring public string Acronym { get; set; } bool IEquatable.Equals(IMod other) => other is DeserializedMod them && Equals(them); - public bool Equals(DeserializedMod other) => ((IMod)this).Equals(other); + public bool Equals(DeserializedMod other) => Acronym == other?.Acronym; } public override string ToString() => $"{User} playing {Beatmap}"; From 9d9c0df64c9213757c51f379ee6be56cce68b87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 17:17:44 +0100 Subject: [PATCH 04/65] Make DeserializedMod equality members match other IMods --- osu.Game/Scoring/ScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 1e5742c358..3084afb833 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -256,7 +256,7 @@ namespace osu.Game.Scoring { public string Acronym { get; set; } - bool IEquatable.Equals(IMod other) => other is DeserializedMod them && Equals(them); + public bool Equals(IMod other) => other is DeserializedMod them && Equals(them); public bool Equals(DeserializedMod other) => Acronym == other?.Acronym; } From f49481e308b68176e560a283870d6dbcbd43c581 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Mar 2021 12:48:21 +0300 Subject: [PATCH 05/65] Add old skin spinner SPM background for testing --- .../Resources/old-skin/spinner-rpm.png | Bin 0 -> 10583 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png new file mode 100644 index 0000000000000000000000000000000000000000..73753554f74fab988c7e5d8c7722d22928120682 GIT binary patch literal 10583 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 Date: Fri, 26 Mar 2021 13:09:44 +0300 Subject: [PATCH 06/65] Refactor spinner SPM counter for skinning purposes --- .../Mods/TestSceneOsuModAutoplay.cs | 10 +-- .../Mods/TestSceneOsuModSpunOut.cs | 4 +- .../TestSceneSpinnerRotation.cs | 10 +-- .../Objects/Drawables/DrawableSpinner.cs | 48 +++++------- .../Skinning/Default/DefaultSpinner.cs | 64 ++++++++++++++++ ...rSpmCounter.cs => SpinnerSpmCalculator.cs} | 74 +++++-------------- 6 files changed, 114 insertions(+), 96 deletions(-) rename osu.Game.Rulesets.Osu/Skinning/Default/{SpinnerSpmCounter.cs => SpinnerSpmCalculator.cs} (61%) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs index 856b6554b9..0ba775e5c7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private void runSpmTest(Mod mod) { - SpinnerSpmCounter spmCounter = null; + SpinnerSpmCalculator spmCalculator = null; CreateModTest(new ModTestData { @@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1 }); - AddUntilStep("fetch SPM counter", () => + AddUntilStep("fetch SPM calculator", () => { - spmCounter = this.ChildrenOfType().SingleOrDefault(); - return spmCounter != null; + spmCalculator = this.ChildrenOfType().SingleOrDefault(); + return spmCalculator != null; }); - AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5)); + AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5)); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 7df5ca0f7c..24e69703a6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Beatmap = singleSpinnerBeatmap, PassCondition = () => { - var counter = Player.ChildrenOfType().SingleOrDefault(); - return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1); + var counter = Player.ChildrenOfType().SingleOrDefault(); + return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1); } }); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index ac8d5c81bc..14c709cae1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests double estimatedSpm = 0; addSeekStep(1000); - AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); + AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value); addSeekStep(2000); - AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); addSeekStep(1000); - AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); } [TestCase(0.5)] @@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve spinner state", () => { expectedProgress = drawableSpinner.Progress; - expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute; + expectedSpm = drawableSpinner.SpinsPerMinute.Value; }); addSeekStep(0); @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests 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)); + AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); } private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 39e78a14aa..1a89fc308e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; public SpinnerRotationTracker RotationTracker { get; private set; } - public SpinnerSpmCounter SpmCounter { get; private set; } + + private SpinnerSpmCalculator spmCalculator; private Container ticks; private PausableSkinnableSound spinningSample; @@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public IBindable GainedBonus => gainedBonus; - private readonly Bindable gainedBonus = new Bindable(); + private readonly Bindable gainedBonus = new BindableDouble(); + + /// + /// The number of spins per minute this spinner is spinning at, for display purposes. + /// + public readonly IBindable SpinsPerMinute = new BindableDouble(); private const double fade_out_duration = 160; @@ -63,7 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + AddInternal(spmCalculator = new SpinnerSpmCalculator + { + Result = { BindTarget = SpinsPerMinute }, + }); + + AddRangeInternal(new Drawable[] { ticks = new Container(), new AspectContainer @@ -77,20 +88,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RotationTracker = new SpinnerRotationTracker(this) } }, - SpmCounter = new SpinnerSpmCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Y = 120, - Alpha = 0 - }, spinningSample = new PausableSkinnableSound { Volume = { Value = 0 }, Looping = true, Frequency = { Value = spinning_sample_initial_frequency } } - }; + }); PositionBindable.BindValueChanged(pos => Position = pos.NewValue); } @@ -161,17 +165,6 @@ 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); @@ -282,22 +275,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateAfterChildren(); - if (!SpmCounter.IsPresent && RotationTracker.Tracking) - { - Result.TimeStarted ??= Time.Current; - fadeInCounter(); - } + if (Result.TimeStarted == null && RotationTracker.Tracking) + Result.TimeStarted = Time.Current; // 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); + spmCalculator.SetRotation(Result.RateAdjustedRotation); updateBonusScore(); } - private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn); - private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; private int wholeSpins; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs index 891821fe2f..ae8c03dad1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private OsuSpriteText bonusCounter; + private Container spmContainer; + private OsuSpriteText spmCounter; + public DefaultSpinner() { RelativeSizeAxes = Axes.Both; @@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Origin = Anchor.Centre, Font = OsuFont.Numeric.With(size: 24), Y = -120, + }, + spmContainer = new Container + { + Alpha = 0f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = 120, + Children = new[] + { + spmCounter = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = @"0", + Font = OsuFont.Numeric.With(size: 24) + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = @"SPINS PER MINUTE", + Font = OsuFont.Numeric.With(size: 12), + Y = 30 + } + } } }); } private IBindable gainedBonus; + private IBindable spinsPerMinute; protected override void LoadComplete() { @@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default bonusCounter.FadeOutFromOne(1500); bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); }); + + spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy(); + spinsPerMinute.BindValueChanged(spm => + { + spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0"); + }, true); + + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); + } + + protected override void Update() + { + base.Update(); + + if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null) + fadeCounterOnTimeStart(); + } + + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + { + if (!(drawableHitObject is DrawableSpinner)) + return; + + fadeCounterOnTimeStart(); + } + + private void fadeCounterOnTimeStart() + { + if (drawableSpinner.Result?.TimeStarted is double startTime) + { + using (BeginAbsoluteSequence(startTime)) + spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn); + } } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs similarity index 61% rename from osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs index 69355f624b..a5205bbb8c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCalculator.cs @@ -1,77 +1,37 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; 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 + public class SpinnerSpmCalculator : Component { + private readonly Queue records = new Queue(); + private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues + + /// + /// The resultant spins per minute value, which is updated via . + /// + public IBindable Result => result; + + private readonly Bindable result = new BindableDouble(); + [Resolved] private DrawableHitObject drawableSpinner { get; set; } - private readonly OsuSpriteText spmText; - - public SpinnerSpmCounter() - { - Children = new Drawable[] - { - spmText = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = @"0", - Font = OsuFont.Numeric.With(size: 24) - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = @"SPINS PER MINUTE", - Font = OsuFont.Numeric.With(size: 12), - Y = 30 - } - }; - } - protected override void LoadComplete() { base.LoadComplete(); drawableSpinner.HitObjectApplied += resetState; } - private double spm; - - public double SpinsPerMinute - { - get => spm; - private set - { - if (value == spm) return; - - spm = value; - spmText.Text = Math.Truncate(value).ToString(@"#0"); - } - } - - private struct RotationRecord - { - public float Rotation; - public double Time; - } - - private readonly Queue records = new Queue(); - private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues - public void SetRotation(float currentRotation) { // Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result. @@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration) record = records.Dequeue(); - SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360; + result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360; } records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current }); @@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private void resetState(DrawableHitObject hitObject) { - SpinsPerMinute = 0; + result.Value = 0; records.Clear(); } @@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (drawableSpinner != null) drawableSpinner.HitObjectApplied -= resetState; } + + private struct RotationRecord + { + public float Rotation; + public double Time; + } } } From f848ef534747babe4c020e3b1a759b2fc42d259d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 26 Mar 2021 13:10:04 +0300 Subject: [PATCH 07/65] Add legacy spinner SPM counter support --- .../Skinning/Legacy/LegacySpinner.cs | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 064b7a4680..7eb6898abc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected const float SPRITE_SCALE = 0.625f; + private const float spm_hide_offset = 50f; + protected DrawableSpinner DrawableSpinner { get; private set; } private Sprite spin; @@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private LegacySpriteText bonusCounter; + private Sprite spmBackground; + private LegacySpriteText spmCounter; + [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) { @@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Scale = new Vector2(SPRITE_SCALE), Y = SPINNER_TOP_OFFSET + 299, }.With(s => s.Font = s.Font.With(fixedWidth: false)), + spmBackground = new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft, + Texture = source.GetTexture("spinner-rpm"), + Scale = new Vector2(SPRITE_SCALE), + Position = new Vector2(-87, 445 + spm_hide_offset), + }, + spmCounter = new LegacySpriteText(source, LegacyFont.Score) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight, + Scale = new Vector2(SPRITE_SCALE * 0.9f), + Position = new Vector2(80, 448 + spm_hide_offset), + }.With(s => s.Font = s.Font.With(fixedWidth: false)), } }); } private IBindable gainedBonus; + private IBindable spinsPerMinute; private readonly Bindable completed = new Bindable(); @@ -99,6 +120,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out); }); + spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy(); + spinsPerMinute.BindValueChanged(spm => + { + spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0"); + }, true); + completed.BindValueChanged(onCompletedChanged, true); DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; @@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy switch (drawableHitObject) { case DrawableSpinner d: - double fadeOutLength = Math.Min(400, d.HitObject.Duration); + using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn)) + { + spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); + spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); + } - using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true)) - spin.FadeOutFromOne(fadeOutLength); + double spinFadeOutLength = Math.Min(400, d.HitObject.Duration); + + using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true)) + spin.FadeOutFromOne(spinFadeOutLength); break; case DrawableSpinnerTick d: From 9504fe3f3c1fb78b8b4f6510ec988933f26bd71d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 13:43:05 +0900 Subject: [PATCH 08/65] Inline add of spm calculation (no need for it to be a separate call) --- .../Objects/Drawables/DrawableSpinner.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 1a89fc308e..3a4753761a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -69,13 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; - AddInternal(spmCalculator = new SpinnerSpmCalculator - { - Result = { BindTarget = SpinsPerMinute }, - }); - AddRangeInternal(new Drawable[] { + spmCalculator = new SpinnerSpmCalculator + { + Result = { BindTarget = SpinsPerMinute }, + }, ticks = new Container(), new AspectContainer { From 648a9d52584896db0cd3ce3cc7b5bb350bb46d32 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 23:15:09 +0900 Subject: [PATCH 09/65] Add multiplayer spectator player grid --- .../Multiplayer/Spectate/PlayerGrid.cs | 142 ++++++++++++++++++ .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 78 ++++++++++ .../Multiplayer/Spectate/PlayerGrid_Facade.cs | 19 +++ 3 files changed, 239 insertions(+) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs new file mode 100644 index 0000000000..a8bd1db9dc --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public partial class PlayerGrid : CompositeDrawable + { + private const float player_spacing = 5; + + public Drawable MaximisedFacade => maximisedFacade; + + private readonly PlayerGridFacade maximisedFacade; + private readonly Container paddingContainer; + private readonly FillFlowContainer facadeContainer; + private readonly Container cellContainer; + + public PlayerGrid() + { + InternalChildren = new Drawable[] + { + paddingContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(player_spacing), + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Child = facadeContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(player_spacing), + } + }, + maximisedFacade = new PlayerGridFacade { RelativeSizeAxes = Axes.Both } + } + }, + cellContainer = new Container { RelativeSizeAxes = Axes.Both } + }; + } + + public void AddContent(Drawable content) + { + var facade = new PlayerGridFacade(); + facadeContainer.Add(facade); + + var cell = new Cell(content) { ToggleMaximisationState = toggleMaximisationState }; + cell.SetFacade(facade); + + cellContainer.Add(cell); + } + + // A depth value that gets decremented every time a new instance is maximised in order to reduce underlaps. + private float maximisedInstanceDepth; + + private void toggleMaximisationState(Cell target) + { + // Iterate through all cells to ensure only one is maximised at any time. + foreach (var i in cellContainer) + { + if (i == target) + i.IsMaximised = !i.IsMaximised; + else + i.IsMaximised = false; + + if (i.IsMaximised) + { + // Transfer cell to the maximised facade. + i.SetFacade(maximisedFacade); + cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f); + } + else + { + // Transfer cell back to its original facade. + i.SetFacade(facadeContainer[cellContainer.IndexOf(target)]); + } + } + } + + protected override void Update() + { + base.Update(); + + Vector2 cellsPerDimension; + + switch (facadeContainer.Count) + { + case 1: + cellsPerDimension = Vector2.One; + break; + + case 2: + cellsPerDimension = new Vector2(2, 1); + break; + + case 3: + case 4: + cellsPerDimension = new Vector2(2); + break; + + case 5: + case 6: + cellsPerDimension = new Vector2(3, 2); + break; + + case 7: + case 8: + case 9: + // 3 rows / 3 cols. + cellsPerDimension = new Vector2(3); + break; + + case 10: + case 11: + case 12: + // 3 rows / 4 cols. + cellsPerDimension = new Vector2(4, 3); + break; + + default: + // 4 rows / 4 cols. + cellsPerDimension = new Vector2(4); + break; + } + + // Total spacing between cells + Vector2 totalCellSpacing = player_spacing * (cellsPerDimension - Vector2.One); + + Vector2 fullSize = paddingContainer.ChildSize - totalCellSpacing; + Vector2 cellSize = Vector2.Divide(fullSize, new Vector2(cellsPerDimension.X, cellsPerDimension.Y)); + + foreach (var cell in facadeContainer) + cell.Size = cellSize; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs new file mode 100644 index 0000000000..1f6e718aa7 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.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 JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public partial class PlayerGrid + { + private class Cell : CompositeDrawable + { + public Action ToggleMaximisationState; + public bool IsMaximised; + + private PlayerGridFacade facade; + private bool isTracking = true; + + public Cell(Drawable content) + { + Origin = Anchor.Centre; + + InternalChild = content; + } + + protected override void Update() + { + base.Update(); + + if (isTracking) + { + Position = getFinalPosition(); + Size = getFinalSize(); + } + } + + public void SetFacade([NotNull] PlayerGridFacade newFacade) + { + PlayerGridFacade lastFacade = facade; + facade = newFacade; + + if (lastFacade == null || lastFacade == newFacade) + return; + + isTracking = false; + + this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint) + .Then() + .OnComplete(_ => + { + if (facade == newFacade) + isTracking = true; + }); + } + + private Vector2 getFinalPosition() + { + var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero)); + return topLeft + facade.DrawSize / 2; + } + + private Vector2 getFinalSize() => facade.DrawSize; + + // Todo: Temporary? + protected override bool ShouldBeConsideredForInput(Drawable child) => false; + + protected override bool OnClick(ClickEvent e) + { + ToggleMaximisationState(this); + return true; + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs new file mode 100644 index 0000000000..c565e2fec6 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public partial class PlayerGrid + { + private class PlayerGridFacade : Drawable + { + public PlayerGridFacade() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + } + } +} From 024adb699c44b5748eebc77a33c2010eb841f562 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 00:06:14 +0900 Subject: [PATCH 10/65] Add test and fix several issues --- ...TestSceneMultiplayerSpectatorPlayerGrid.cs | 115 ++++++++++++++++++ .../Multiplayer/Spectate/PlayerGrid.cs | 30 +++-- .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 26 +++- .../Multiplayer/Spectate/PlayerGrid_Facade.cs | 7 +- 4 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.cs new file mode 100644 index 0000000000..c0958c7fe8 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorPlayerGrid.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerSpectatorPlayerGrid : OsuManualInputManagerTestScene + { + private PlayerGrid grid; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }; + }); + + [Test] + public void TestMaximiseAndMinimise() + { + addCells(2); + + assertMaximisation(0, false, true); + assertMaximisation(1, false, true); + + clickCell(0); + assertMaximisation(0, true); + assertMaximisation(1, false, true); + clickCell(0); + assertMaximisation(0, false); + assertMaximisation(1, false, true); + + clickCell(1); + assertMaximisation(1, true); + assertMaximisation(0, false, true); + clickCell(1); + assertMaximisation(1, false); + assertMaximisation(0, false, true); + } + + [Test] + public void TestClickBothCellsSimultaneously() + { + addCells(2); + + AddStep("click cell 0 then 1", () => + { + InputManager.MoveMouseTo(grid.Content.ElementAt(0)); + InputManager.Click(MouseButton.Left); + + InputManager.MoveMouseTo(grid.Content.ElementAt(1)); + InputManager.Click(MouseButton.Left); + }); + + assertMaximisation(1, true); + assertMaximisation(0, false); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(9)] + [TestCase(11)] + [TestCase(12)] + [TestCase(15)] + [TestCase(16)] + public void TestCellCount(int count) + { + addCells(count); + AddWaitStep("wait for display", 2); + } + + private void addCells(int count) => AddStep($"add {count} grid cells", () => + { + for (int i = 0; i < count; i++) + grid.Add(new GridContent()); + }); + + private void clickCell(int index) => AddStep($"click cell index {index}", () => + { + InputManager.MoveMouseTo(grid.Content.ElementAt(index)); + InputManager.Click(MouseButton.Left); + }); + + private void assertMaximisation(int index, bool shouldBeMaximised, bool instant = false) + { + string assertionText = $"cell index {index} {(shouldBeMaximised ? "is" : "is not")} maximised"; + + if (instant) + AddAssert(assertionText, checkAction); + else + AddUntilStep(assertionText, checkAction); + + bool checkAction() => Precision.AlmostEquals(grid.MaximisedFacade.DrawSize, grid.Content.ElementAt(index).DrawSize, 10) == shouldBeMaximised; + } + + private class GridContent : Box + { + public GridContent() + { + RelativeSizeAxes = Axes.Both; + Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index a8bd1db9dc..f41948217c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -13,9 +13,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public Drawable MaximisedFacade => maximisedFacade; - private readonly PlayerGridFacade maximisedFacade; + private readonly Facade maximisedFacade; private readonly Container paddingContainer; - private readonly FillFlowContainer facadeContainer; + private readonly FillFlowContainer facadeContainer; private readonly Container cellContainer; public PlayerGrid() @@ -31,38 +31,50 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate new Container { RelativeSizeAxes = Axes.Both, - Child = facadeContainer = new FillFlowContainer + Child = facadeContainer = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(player_spacing), } }, - maximisedFacade = new PlayerGridFacade { RelativeSizeAxes = Axes.Both } + maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both } } }, cellContainer = new Container { RelativeSizeAxes = Axes.Both } }; } - public void AddContent(Drawable content) + /// + /// Adds a new cell with content to this grid. + /// + /// The content the cell should contain. + /// If more than 16 cells are added. + public void Add(Drawable content) { - var facade = new PlayerGridFacade(); + int index = cellContainer.Count; + + var facade = new Facade(); facadeContainer.Add(facade); - var cell = new Cell(content) { ToggleMaximisationState = toggleMaximisationState }; + var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState }; cell.SetFacade(facade); cellContainer.Add(cell); } + /// + /// The content added to this grid. + /// + public IEnumerable Content => cellContainer.OrderBy(c => c.FacadeIndex).Select(c => c.Content); + // A depth value that gets decremented every time a new instance is maximised in order to reduce underlaps. private float maximisedInstanceDepth; private void toggleMaximisationState(Cell target) { // Iterate through all cells to ensure only one is maximised at any time. - foreach (var i in cellContainer) + foreach (var i in cellContainer.ToList()) { if (i == target) i.IsMaximised = !i.IsMaximised; @@ -78,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate else { // Transfer cell back to its original facade. - i.SetFacade(facadeContainer[cellContainer.IndexOf(target)]); + i.SetFacade(facadeContainer[i.FacadeIndex]); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index 1f6e718aa7..c2ac190d40 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -14,17 +14,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { private class Cell : CompositeDrawable { + /// + /// The index of the original facade of this cell. + /// + public readonly int FacadeIndex; + + /// + /// The contained content. + /// + public readonly Drawable Content; + public Action ToggleMaximisationState; public bool IsMaximised; - private PlayerGridFacade facade; + private Facade facade; private bool isTracking = true; - public Cell(Drawable content) + public Cell(int facadeIndex, Drawable content) { - Origin = Anchor.Centre; + FacadeIndex = facadeIndex; - InternalChild = content; + Origin = Anchor.Centre; + InternalChild = Content = content; } protected override void Update() @@ -38,9 +49,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - public void SetFacade([NotNull] PlayerGridFacade newFacade) + /// + /// Makes this cell track a new facade. + /// + public void SetFacade([NotNull] Facade newFacade) { - PlayerGridFacade lastFacade = facade; + Facade lastFacade = facade; facade = newFacade; if (lastFacade == null || lastFacade == newFacade) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs index c565e2fec6..6b363c6040 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Facade.cs @@ -7,9 +7,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public partial class PlayerGrid { - private class PlayerGridFacade : Drawable + /// + /// A facade of the grid which is used as a dummy object to store the required position/size of cells. + /// + private class Facade : Drawable { - public PlayerGridFacade() + public Facade() { Anchor = Anchor.Centre; Origin = Anchor.Centre; From 5dc939c2f365d09395f0fe1880bb88a3d8dbda21 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 00:06:32 +0900 Subject: [PATCH 11/65] More documentation --- .../OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs | 15 ++++++++++++++- .../Multiplayer/Spectate/PlayerGrid_Cell.cs | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs index f41948217c..830378f129 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs @@ -1,16 +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 osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { + /// + /// A grid of players playing the multiplayer match. + /// public partial class PlayerGrid : CompositeDrawable { private const float player_spacing = 5; + /// + /// The currently-maximised facade. + /// public Drawable MaximisedFacade => maximisedFacade; private readonly Facade maximisedFacade; @@ -52,6 +61,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// If more than 16 cells are added. public void Add(Drawable content) { + if (cellContainer.Count == 16) + throw new InvalidOperationException("Only 16 cells are supported."); + int index = cellContainer.Count; var facade = new Facade(); @@ -99,6 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { base.Update(); + // Different layouts are used for varying cell counts in order to maximise dimensions. Vector2 cellsPerDimension; switch (facadeContainer.Count) @@ -141,7 +154,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate break; } - // Total spacing between cells + // Total inter-cell spacing. Vector2 totalCellSpacing = player_spacing * (cellsPerDimension - Vector2.One); Vector2 fullSize = paddingContainer.ChildSize - totalCellSpacing; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index c2ac190d40..37d88693ee 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -12,6 +12,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public partial class PlayerGrid { + /// + /// A cell of the grid. Contains the content and tracks to the linked facade. + /// private class Cell : CompositeDrawable { /// @@ -24,7 +27,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public readonly Drawable Content; + /// + /// An action that toggles the maximisation state of this cell. + /// public Action ToggleMaximisationState; + + /// + /// Whether this cell is currently maximised. + /// public bool IsMaximised; private Facade facade; From bb15baf118f6e49306acc9688cd6790b39dc102c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 17:31:14 +0900 Subject: [PATCH 12/65] Add initial multiplayer spectator leaderboard --- .../MultiplayerSpectatorLeaderboard.cs | 82 +++++++++++++++++++ .../HUD/MultiplayerGameplayLeaderboard.cs | 19 +++-- 2 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs new file mode 100644 index 0000000000..35c3471ce2 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.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.Collections.Generic; +using System.Linq; +using osu.Framework.Timing; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +{ + public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard + { + private readonly Dictionary trackedData = new Dictionary(); + + public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + : base(scoreProcessor, userIds) + { + } + + public void AddSource(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); + + public void RemoveSource(int userId, IClock source) => trackedData.Remove(userId); + + protected override void OnIncomingFrames(int userId, FrameDataBundle bundle) + { + if (!trackedData.TryGetValue(userId, out var data)) + return; + + data.Frames.Add(new TimedFrameHeader(bundle.Frames.First().Time, bundle.Header)); + } + + protected override void Update() + { + base.Update(); + + foreach (var (userId, data) in trackedData) + { + var targetTime = data.Clock.CurrentTime; + + int frameIndex = data.Frames.BinarySearch(new TimedFrameHeader(targetTime)); + if (frameIndex < 0) + frameIndex = ~frameIndex; + frameIndex = Math.Clamp(frameIndex - 1, 0, data.Frames.Count - 1); + + SetCurrentFrame(userId, data.Frames[frameIndex].Header); + } + } + + private class TrackedUserData + { + public readonly IClock Clock; + public readonly List Frames = new List(); + + public TrackedUserData(IClock clock) + { + Clock = clock; + } + } + + private class TimedFrameHeader : IComparable + { + public readonly double Time; + public readonly FrameHeader Header; + + public TimedFrameHeader(double time) + { + Time = time; + } + + public TimedFrameHeader(double time, FrameHeader header) + { + Time = time; + Header = header; + } + + public int CompareTo(TimedFrameHeader other) => Time.CompareTo(other.Time); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index a3d27c4e71..65ad8be3f0 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -20,7 +20,6 @@ namespace osu.Game.Screens.Play.HUD public class MultiplayerGameplayLeaderboard : GameplayLeaderboard { private readonly ScoreProcessor scoreProcessor; - private readonly Dictionary userScores = new Dictionary(); [Resolved] @@ -116,13 +115,19 @@ namespace osu.Game.Screens.Play.HUD trackedData.UpdateScore(scoreProcessor, mode.NewValue); } - private void handleIncomingFrames(int userId, FrameDataBundle bundle) + private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() => { - if (userScores.TryGetValue(userId, out var trackedData)) - { - trackedData.LastHeader = bundle.Header; - trackedData.UpdateScore(scoreProcessor, scoringMode.Value); - } + if (userScores.ContainsKey(userId)) + OnIncomingFrames(userId, bundle); + }); + + protected virtual void OnIncomingFrames(int userId, FrameDataBundle bundle) => SetCurrentFrame(userId, bundle.Header); + + protected void SetCurrentFrame(int userId, FrameHeader header) + { + var trackedScore = userScores[userId]; + trackedScore.LastHeader = header; + trackedScore.UpdateScore(scoreProcessor, scoringMode.Value); } protected override void Dispose(bool isDisposing) From 3b86f0eb2f55839c4301cba1aefcc971ba205149 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:15:23 +0900 Subject: [PATCH 13/65] Fix exception with 0 frames --- .../Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs index 35c3471ce2..08db5befa4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -40,6 +40,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { var targetTime = data.Clock.CurrentTime; + if (data.Frames.Count == 0) + continue; + int frameIndex = data.Frames.BinarySearch(new TimedFrameHeader(targetTime)); if (frameIndex < 0) frameIndex = ~frameIndex; From 90e243eea5404e1632fc3f63c679c8fc38874c60 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:15:27 +0900 Subject: [PATCH 14/65] Rename methods --- .../Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs index 08db5befa4..f16869b43d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -20,9 +20,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - public void AddSource(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); + public void AddClock(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); - public void RemoveSource(int userId, IClock source) => trackedData.Remove(userId); + public void RemoveClock(int userId, IClock source) => trackedData.Remove(userId); protected override void OnIncomingFrames(int userId, FrameDataBundle bundle) { From 589ce4bdc2b09c7f96d99582aea7e7f8ef430eb8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:16:10 +0900 Subject: [PATCH 15/65] Add test --- ...estSceneMultiplayerSpectatorLeaderboard.cs | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs new file mode 100644 index 0000000000..57e9fb37cc --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -0,0 +1,207 @@ +// Copyright (c) ppy 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 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.Framework.Timing; +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; +using osu.Game.Scoring; +using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Screens.Play.HUD; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene + { + [Cached(typeof(SpectatorStreamingClient))] + private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); + + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + + protected override Container Content => content; + private readonly Container content; + + private readonly Dictionary clocks = new Dictionary + { + { 55, new ManualClock() }, + { 56, new ManualClock() } + }; + + public TestSceneMultiplayerSpectatorLeaderboard() + { + base.Content.AddRange(new Drawable[] + { + streamingClient, + lookupCache, + content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + + [SetUpSteps] + public new void SetUpSteps() + { + MultiplayerSpectatorLeaderboard leaderboard = null; + + AddStep("reset", () => + { + Clear(); + + foreach (var (userId, clock) in clocks) + { + streamingClient.EndPlay(userId, 0); + clock.CurrentTime = 0; + } + }); + + AddStep("create leaderboard", () => + { + foreach (var (userId, _) in clocks) + streamingClient.StartPlay(userId, 0); + + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + var scoreProcessor = new OsuScoreProcessor(); + scoreProcessor.ApplyBeatmap(playable); + + LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add); + }); + + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + + AddStep("add clock sources", () => + { + foreach (var (userId, clock) in clocks) + leaderboard.AddClock(userId, clock); + }); + } + + [Test] + public void TestLeaderboardTracksCurrentTime() + { + AddStep("send frames", () => + { + // For user 55, send 100 frames in sets of 1. + // For user 56, send 100 frames in sets of 10. + for (int i = 0; i < 100; i++) + { + streamingClient.SendFrames(55, i, 1); + + if (i % 10 == 0) + streamingClient.SendFrames(56, i, 10); + } + }); + + assertCombo(55, 1); + assertCombo(56, 10); + + setTime(500); + assertCombo(55, 5); + assertCombo(56, 10); + + setTime(1100); + assertCombo(55, 11); + assertCombo(56, 20); + } + + private void setTime(double time) => AddStep($"set time {time}", () => + { + foreach (var (_, clock) in clocks) + clock.CurrentTime = time; + }); + + private void assertCombo(int userId, int expectedCombo) + => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo); + + private class TestSpectatorStreamingClient : SpectatorStreamingClient + { + private readonly Dictionary userBeatmapDictionary = new Dictionary(); + private readonly Dictionary userSentStateDictionary = new Dictionary(); + + public TestSpectatorStreamingClient() + : base(new DevelopmentEndpointConfiguration()) + { + } + + public void StartPlay(int userId, int beatmapId) + { + userBeatmapDictionary[userId] = beatmapId; + userSentStateDictionary[userId] = false; + sendState(userId, beatmapId); + } + + public void EndPlay(int userId, int beatmapId) + { + ((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + userSentStateDictionary[userId] = false; + } + + public void SendFrames(int userId, int index, int count) + { + var frames = new List(); + + for (int i = index; i < index + count; i++) + { + var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; + frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); + } + + var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames); + ((ISpectatorClient)this).UserSentFrames(userId, bundle); + if (!userSentStateDictionary[userId]) + sendState(userId, userBeatmapDictionary[userId]); + } + + public override void WatchUser(int userId) + { + if (userSentStateDictionary[userId]) + { + // usually the server would do this. + sendState(userId, userBeatmapDictionary[userId]); + } + + base.WatchUser(userId); + } + + private void sendState(int userId, int beatmapId) + { + ((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + userSentStateDictionary[userId] = true; + } + } + + private class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + { + return Task.FromResult(new User + { + Id = lookup, + Username = $"User {lookup}" + }); + } + } + } +} From b49997f531dad5e80f50fa6cbe1f4349a99be38f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:18:23 +0900 Subject: [PATCH 16/65] Add test for no frames --- .../TestSceneMultiplayerSpectatorLeaderboard.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs index 57e9fb37cc..53efb4392f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -118,6 +118,13 @@ namespace osu.Game.Tests.Visual.Multiplayer assertCombo(56, 20); } + [Test] + public void TestNoFrames() + { + assertCombo(55, 0); + assertCombo(56, 1); + } + private void setTime(double time) => AddStep($"set time {time}", () => { foreach (var (_, clock) in clocks) From 9ddcd686ac09e3f0aa35c0f5a9c9ae01e7e44f0e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:23:38 +0900 Subject: [PATCH 17/65] Fix incorrect assert --- .../Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs index 53efb4392f..8589cffcc9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestNoFrames() { assertCombo(55, 0); - assertCombo(56, 1); + assertCombo(56, 0); } private void setTime(double time) => AddStep($"set time {time}", () => From e73f3f52d7730854a9f360dc2f1ae7df0ec834ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:23:41 +0900 Subject: [PATCH 18/65] Add some more asserts --- ...estSceneMultiplayerSpectatorLeaderboard.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs index 8589cffcc9..3b2cfb1c7b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectatorLeaderboard.cs @@ -95,8 +95,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("send frames", () => { - // For user 55, send 100 frames in sets of 1. - // For user 56, send 100 frames in sets of 10. + // For user 55, send frames in sets of 1. + // For user 56, send frames in sets of 10. for (int i = 0; i < 100; i++) { streamingClient.SendFrames(55, i, 1); @@ -109,13 +109,25 @@ namespace osu.Game.Tests.Visual.Multiplayer assertCombo(55, 1); assertCombo(56, 10); + // Advance to a point where only user 55's frame changes. setTime(500); assertCombo(55, 5); assertCombo(56, 10); + // Advance to a point where both user's frame changes. setTime(1100); assertCombo(55, 11); assertCombo(56, 20); + + // Advance user 56 only to a point where its frame changes. + setTime(56, 2100); + assertCombo(55, 11); + assertCombo(56, 30); + + // Advance both users beyond their last frame + setTime(101 * 100); + assertCombo(55, 100); + assertCombo(56, 100); } [Test] @@ -131,6 +143,9 @@ namespace osu.Game.Tests.Visual.Multiplayer clock.CurrentTime = time; }); + private void setTime(int userId, double time) + => AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time); + private void assertCombo(int userId, int expectedCombo) => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo); From 7cbc8f2695b4868fa4be7a9b41ebd8ef42b1b0d4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:29:02 +0900 Subject: [PATCH 19/65] Add some xmldocs --- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 65ad8be3f0..f0d2ac4b7f 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -121,8 +121,21 @@ namespace osu.Game.Screens.Play.HUD OnIncomingFrames(userId, bundle); }); + /// + /// Invoked when new frames have arrived for a user. + /// + /// + /// By default, this immediately sets the current frame to be displayed for the user. + /// + /// The user which the frames arrived for. + /// The bundle of frames. protected virtual void OnIncomingFrames(int userId, FrameDataBundle bundle) => SetCurrentFrame(userId, bundle.Header); + /// + /// Sets the current frame to be displayed for a user. + /// + /// The user to set the frame of. + /// The frame to set. protected void SetCurrentFrame(int userId, FrameHeader header) { var trackedScore = userScores[userId]; From d2c37e6cf8828e5793c9aef36b76fd6ce986b537 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 18:41:58 +0900 Subject: [PATCH 20/65] Remove unnecessary parameter --- .../Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs index f16869b43d..aa9f162036 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public void AddClock(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); - public void RemoveClock(int userId, IClock source) => trackedData.Remove(userId); + public void RemoveClock(int userId) => trackedData.Remove(userId); protected override void OnIncomingFrames(int userId, FrameDataBundle bundle) { From fbd5195738bdd9843434ea7462a906e928285d5a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 12 Apr 2021 03:37:03 +0300 Subject: [PATCH 21/65] Extract mod setting value handling to utils class --- .../API/ModSettingsDictionaryFormatter.cs | 34 ++---------------- osu.Game/Utils/ModUtils.cs | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index dd854acc32..81ecc74ddb 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -3,11 +3,10 @@ using System.Buffers; using System.Collections.Generic; -using System.Diagnostics; using System.Text; using MessagePack; using MessagePack.Formatters; -using osu.Framework.Bindables; +using osu.Game.Utils; namespace osu.Game.Online.API { @@ -24,36 +23,7 @@ namespace osu.Game.Online.API var stringBytes = new ReadOnlySequence(Encoding.UTF8.GetBytes(kvp.Key)); writer.WriteString(in stringBytes); - switch (kvp.Value) - { - case Bindable d: - 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; - - case Bindable b: - 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); - break; - } + primitiveFormatter.Serialize(ref writer, ModUtils.GetSettingUnderlyingValue(kvp.Value), options); } } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index c12b5a9fd4..596880f2e7 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Online.API; using osu.Game.Rulesets.Mods; #nullable enable @@ -129,5 +132,38 @@ namespace osu.Game.Utils else yield return mod; } + + /// + /// Returns the underlying value of the given mod setting object. + /// Used in for serialization and equality comparison purposes. + /// + /// The mod setting. + public static object GetSettingUnderlyingValue(object setting) + { + switch (setting) + { + case Bindable d: + return d.Value; + + case Bindable i: + return i.Value; + + case Bindable f: + return f.Value; + + case Bindable b: + return b.Value; + + case IBindable u: + // A mod with unknown (e.g. enum) generic type. + var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); + Debug.Assert(valueMethod != null); + return valueMethod.GetValue(u); + + default: + // fall back for non-bindable cases. + return setting; + } + } } } From 4fcddfb44b7284dcf39720a685d2f34813e56ecd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 13:42:14 +0900 Subject: [PATCH 22/65] Fix multiplayer test failure --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 839118de2f..caa731f985 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); + AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); } } } From 995c244ceef985c3b381b6ec6af54f8d5d940b0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 14:00:26 +0900 Subject: [PATCH 23/65] Remove alt-mousewheel bindings for volume adjustment With the recent changes to the order of processing key bindings (`GlobalAction`s are handled first), having the alt-wheel bindings in here causes a regression as they are handled before `OnScroll` events. Specifically, this means editor alt-scroll functionality no longer works with the default bindings. Removing the bindings fixes this, while also still allowing alt-wheel adjustment of the volume via `VolumeControlReceptor`: https://github.com/ppy/osu/blob/a2f50af4243dfde95ec556859666b65e34f3007b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs#L21-L26 In conjunction with the special case in `OsuScrollContainer`: https://github.com/ppy/osu/blob/02d5b1352b6c9971ea83a131c0d2637e167b56b3/osu.Game/Graphics/Containers/OsuScrollContainer.cs#L103-L105 --- .../Input/Bindings/GlobalActionContainer.cs | 2 - ...700_RefreshVolumeBindingsAgain.Designer.cs | 506 ++++++++++++++++++ ...210412045700_RefreshVolumeBindingsAgain.cs | 16 + 3 files changed, 522 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs create mode 100644 osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 671c3bc8bc..ba4d757cd7 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -97,9 +97,7 @@ namespace osu.Game.Input.Bindings public IEnumerable AudioControlKeyBindings => new[] { new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume), - new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.IncreaseVolume), new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume), - new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume), new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute), diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs new file mode 100644 index 0000000000..2c100d39b9 --- /dev/null +++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20210412045700_RefreshVolumeBindingsAgain")] + partial class RefreshVolumeBindingsAgain + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.Property("VideoFile"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs new file mode 100644 index 0000000000..155d6670a8 --- /dev/null +++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class RefreshVolumeBindingsAgain : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} From dd1925aaedc756a57e1aecadbe2e36992bd17e8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 14:29:27 +0900 Subject: [PATCH 24/65] Remove temporary input ignore --- .../Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs index 37d88693ee..2df05cb5ed 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid_Cell.cs @@ -89,9 +89,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private Vector2 getFinalSize() => facade.DrawSize; - // Todo: Temporary? - protected override bool ShouldBeConsideredForInput(Drawable child) => false; - protected override bool OnClick(ClickEvent e) { ToggleMaximisationState(this); From 0eab9daf13bb406d2b82c1f624dbb7e483f0ab40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 16:41:36 +0900 Subject: [PATCH 25/65] Update existing overlay containers to not block scroll input --- osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs | 2 ++ .../Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs | 2 ++ osu.Game/Screens/Play/GameplayMenuOverlay.cs | 2 ++ osu.Game/Screens/Play/SongProgress.cs | 2 ++ 4 files changed, 8 insertions(+) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index fbf2ffd4bd..e168f265dd 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers protected virtual string PopInSampleName => "UI/overlay-pop-in"; protected virtual string PopOutSampleName => "UI/overlay-pop-out"; + protected override bool BlockScrollInput => false; + protected override bool BlockNonPositionalInput => true; /// diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs index ea3951fc3b..5699da740c 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs @@ -19,6 +19,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected OnlinePlayComposite Settings { get; set; } + protected override bool BlockScrollInput => false; + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index f938839be3..4a28da0dde 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -31,6 +31,8 @@ namespace osu.Game.Screens.Play protected override bool BlockNonPositionalInput => true; + protected override bool BlockScrollInput => false; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public Action OnRetry; diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index acf4640aa4..6c7cb9376c 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -45,6 +45,8 @@ namespace osu.Game.Screens.Play public override bool HandleNonPositionalInput => AllowSeeking.Value; public override bool HandlePositionalInput => AllowSeeking.Value; + protected override bool BlockScrollInput => false; + private double firstHitTime => objects.First().StartTime; private IEnumerable objects; From b5954a55adc3af47c27ea4c2b354e320d9b6cf4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 17:46:14 +0900 Subject: [PATCH 26/65] Remove empty xmldoc --- osu.Desktop/Program.cs | 1 - .../Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs | 1 - osu.Game.Tournament/TournamentGameBase.cs | 1 - osu.Game/Beatmaps/IBeatmap.cs | 1 - osu.Game/Configuration/SettingsStore.cs | 1 - osu.Game/Graphics/Backgrounds/Triangles.cs | 1 - osu.Game/IO/Serialization/IJsonSerializable.cs | 1 - osu.Game/Input/KeyBindingStore.cs | 1 - osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 1 - osu.Game/Rulesets/Objects/SliderPath.cs | 2 -- osu.Game/Rulesets/Ruleset.cs | 1 - osu.Game/Rulesets/Scoring/HitWindows.cs | 1 - osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs | 1 - osu.Game/Screens/Ranking/ScorePanelList.cs | 1 - osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs | 1 - osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs | 1 - osu.Game/Utils/Optional.cs | 1 - 17 files changed, 18 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index d06c4b6746..5fb09c0cef 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -69,7 +69,6 @@ namespace osu.Desktop /// Allow a maximum of one unhandled exception, per second of execution. /// /// - /// private static bool handleException(Exception arg) { bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index c81710ed18..26e5d381e2 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -482,7 +482,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// Retrieves the sample info list at a point in time. /// /// The time to retrieve the sample info list from. - /// private IList sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples; /// diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 2ee52c35aa..92eb7ac713 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -141,7 +141,6 @@ namespace osu.Game.Tournament /// /// Add missing player info based on user IDs. /// - /// private bool addPlayers() { bool addedInfo = false; diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 9847ea020a..769b33009a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -44,7 +44,6 @@ namespace osu.Game.Beatmaps /// /// Returns statistics for the contained in this beatmap. /// - /// IEnumerable GetStatistics(); /// diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index f8c9bdeaf8..86e84b0732 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -22,7 +22,6 @@ namespace osu.Game.Configuration /// /// The ruleset's internal ID. /// An optional variant. - /// public List Query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 0e9382279a..67cee883c8 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -346,7 +346,6 @@ namespace osu.Game.Graphics.Backgrounds /// such that the smaller triangles appear on top. /// /// - /// public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale); } } diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs index ba188963ea..c8d5ce39a6 100644 --- a/osu.Game/IO/Serialization/IJsonSerializable.cs +++ b/osu.Game/IO/Serialization/IJsonSerializable.cs @@ -22,7 +22,6 @@ namespace osu.Game.IO.Serialization /// /// Creates the default that should be used for all s. /// - /// public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index b25b00eb84..9d0cfedc03 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -85,7 +85,6 @@ namespace osu.Game.Input /// /// The ruleset's internal ID. /// An optional variant. - /// public List Query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 6da9f12b50..d95b246c96 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -574,7 +574,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// 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; diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 61f5f94142..e64298f98d 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -147,7 +147,6 @@ namespace osu.Game.Rulesets.Objects /// to 1 (end of the path). /// /// Ranges from 0 (beginning of the path) to 1 (end of the path). - /// public Vector2 PositionAt(double progress) { ensureValid(); @@ -161,7 +160,6 @@ namespace osu.Game.Rulesets.Objects /// The first point has a PathType which all other points inherit. /// /// One of the control points in the segment. - /// public List PointsInSegment(PathControlPoint controlPoint) { bool found = false; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 38d30a2e31..efc8b50e3c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -146,7 +146,6 @@ namespace osu.Game.Rulesets /// The beatmap to create the hit renderer for. /// The s to apply. /// Unable to successfully load the beatmap to be usable with this ruleset. - /// public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null); /// diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 018b50bd3d..410614de07 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -62,7 +62,6 @@ namespace osu.Game.Rulesets.Scoring /// /// Retrieves a mapping of s to their timing windows for all allowed s. /// - /// public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs index a7b0fb05e3..dcf5f8a788 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs @@ -12,7 +12,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons /// /// Whether this is selected. /// - /// public readonly BindableBool Selected; /// diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 77b3d8fc3b..441c9e048a 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -226,7 +226,6 @@ namespace osu.Game.Screens.Ranking /// /// Enumerates all s contained in this . /// - /// public IEnumerable GetScorePanels() => flow.Select(t => t.Panel); /// diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index fcf20a2eb2..5ef2458919 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -189,7 +189,6 @@ namespace osu.Game.Tests.Beatmaps /// /// Creates the applicable to this . /// - /// protected abstract Ruleset CreateRuleset(); private class ConvertResult diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index 76f97db59f..54a83f4305 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -17,7 +17,6 @@ namespace osu.Game.Tests.Beatmaps /// /// Creates the whose legacy mod conversion is to be tested. /// - /// protected abstract Ruleset CreateRuleset(); protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) diff --git a/osu.Game/Utils/Optional.cs b/osu.Game/Utils/Optional.cs index 9f8a1c2e62..fdb7623be5 100644 --- a/osu.Game/Utils/Optional.cs +++ b/osu.Game/Utils/Optional.cs @@ -37,7 +37,6 @@ namespace osu.Game.Utils /// 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 cc2acf5e548fb11873f1d20d480b27e35ced57fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 19:05:23 +0900 Subject: [PATCH 27/65] Fix ctrl-dragging on an existing selection causing deselection of the hovered object --- .../Compose/Components/BlueprintContainer.cs | 38 ++++++++++++++----- .../Compose/Components/SelectionHandler.cs | 37 +++++++++++++++--- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5699be4560..64cf0e7512 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -135,11 +135,12 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - if (!beginClickSelection(e)) return true; + bool selectionPerformed = beginClickSelection(e); + // even if a selection didn't occur, a drag event may still move the selection. prepareSelectionMovement(); - return e.Button == MouseButton.Left; + return selectionPerformed || e.Button == MouseButton.Left; } private SelectionBlueprint clickedBlueprint; @@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // 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() || clickedBlueprint != null) + if (endClickSelection(e) || clickedBlueprint != null) return true; deselectAll(); @@ -177,7 +178,12 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnMouseUp(MouseUpEvent e) { // Special case for when a drag happened instead of a click - Schedule(() => endClickSelection()); + Schedule(() => + { + endClickSelection(e); + clickSelectionBegan = false; + isDraggingBlueprint = false; + }); finishSelectionMovement(); } @@ -226,7 +232,6 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.Update(obj); changeHandler?.EndChange(); - isDraggingBlueprint = false; } if (DragBox.State == Visibility.Visible) @@ -355,13 +360,28 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Finishes the current blueprint selection. /// + /// The mouse event which triggered end of selection. /// Whether a click selection was active. - private bool endClickSelection() + private bool endClickSelection(MouseButtonEvent e) { - if (!clickSelectionBegan) - return false; + if (!clickSelectionBegan && !isDraggingBlueprint) + { + // if a selection didn't occur, we may want to trigger a deselection. + if (e.ControlPressed && e.Button == MouseButton.Left) + { + // Iterate from the top of the input stack (blueprints closest to the front of the screen first). + // Priority is given to already-selected blueprints. + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected)) + { + if (!blueprint.IsHovered) continue; + + return clickSelectionBegan = SelectionHandler.HandleDeselectionRequested(blueprint, e); + } + } + + return false; + } - clickSelectionBegan = false; return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 018d4d081c..e5e1100797 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -228,12 +228,31 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } - if (e.ControlPressed && e.Button == MouseButton.Left) + // while holding control, we only want to add to selection, not replace an existing selection. + if (e.ControlPressed && e.Button == MouseButton.Left && !blueprint.IsSelected) + { blueprint.ToggleSelection(); - else - ensureSelected(blueprint); + return true; + } - return true; + return ensureSelected(blueprint); + } + + /// + /// Handle a blueprint requesting selection. + /// + /// The blueprint. + /// The mouse event responsible for deselection. + /// Whether a deselection was performed. + internal bool HandleDeselectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) + { + if (blueprint.IsSelected) + { + blueprint.ToggleSelection(); + return true; + } + + return false; } private void handleQuickDeletion(SelectionBlueprint blueprint) @@ -247,13 +266,19 @@ namespace osu.Game.Screens.Edit.Compose.Components deleteSelected(); } - private void ensureSelected(SelectionBlueprint blueprint) + /// + /// Ensure the blueprint is in a selected state. + /// + /// The blueprint to select. + /// Whether selection state was changed. + private bool ensureSelected(SelectionBlueprint blueprint) { if (blueprint.IsSelected) - return; + return false; DeselectAll?.Invoke(); blueprint.Select(); + return true; } private void deleteSelected() From b4c75ba3c6eb81a8cf88f36edc8738ee7e2e09df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 19:19:25 +0900 Subject: [PATCH 28/65] Fix TestQuickDeleteRemovesObject failing on second run --- .../Visual/Editing/TestSceneEditorQuickDelete.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index 9efd299fba..8a0f27b851 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestQuickDeleteRemovesSliderControlPoint() { - Slider slider = new Slider { StartTime = 1000 }; + Slider slider = null; PathControlPoint[] points = { @@ -62,7 +62,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add slider", () => { - slider.Path = new SliderPath(points); + slider = new Slider + { + StartTime = 1000, + Path = new SliderPath(points) + }; + EditorBeatmap.Add(slider); }); From 905cd7c8eb9e7c3785ffa88e63cce5b4b7f2c024 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 19:22:07 +0900 Subject: [PATCH 29/65] 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 196d122a2a..c78dfb6a55 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 71a6f0e5cd..92e05cb4a6 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 a389cc13dd..11124730c9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From d2d7f77430181ce55d76eb7e2ef24613cfe69ca4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 19:50:24 +0900 Subject: [PATCH 30/65] Fix mods not being serialised correctly in ScoreInfo --- .../Online/TestAPIModJsonSerialization.cs | 33 +++++++++++++ osu.Game/Scoring/ScoreInfo.cs | 49 +++++++++---------- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 77f910c144..3afb7481b1 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -11,7 +11,10 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; namespace osu.Game.Tests.Online { @@ -84,6 +87,36 @@ namespace osu.Game.Tests.Online Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11)); } + [Test] + public void TestDeserialiseScoreInfoWithEmptyMods() + { + var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo }; + + var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score)); + + if (deserialised != null) + deserialised.Ruleset = new OsuRuleset().RulesetInfo; + + Assert.That(deserialised?.Mods.Length, Is.Zero); + } + + [Test] + public void TestDeserialiseScoreInfoWithCustomModSetting() + { + var score = new ScoreInfo + { + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } } + }; + + var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score)); + + if (deserialised != null) + deserialised.Ruleset = new OsuRuleset().RulesetInfo; + + Assert.That(((OsuModDoubleTime)deserialised?.Mods[0])?.SpeedChange.Value, Is.EqualTo(2)); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => new Mod[] diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index ef11c19e3f..df8f309ee0 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -55,9 +56,10 @@ namespace osu.Game.Scoring [JsonIgnore] public virtual RulesetInfo Ruleset { get; set; } + private APIMod[] localAPIMods; private Mod[] mods; - [JsonProperty("mods")] + [JsonIgnore] [NotMapped] public Mod[] Mods { @@ -66,45 +68,50 @@ namespace osu.Game.Scoring if (mods != null) return mods; - if (modsJson == null) + if (apiMods == null) return Array.Empty(); - return getModsFromRuleset(JsonConvert.DeserializeObject(modsJson)); + var rulesetInstance = Ruleset.CreateInstance(); + return apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } set { - modsJson = null; + localAPIMods = null; mods = value; } } - private Mod[] getModsFromRuleset(DeserializedMod[] mods) => Ruleset.CreateInstance().GetAllMods().Where(mod => mods.Any(d => d.Acronym == mod.Acronym)).ToArray(); - - private string modsJson; - - [JsonIgnore] - [Column("Mods")] - public string ModsJson + // Used for API serialisation/deserialisation. + [JsonProperty("mods")] + private APIMod[] apiMods { get { - if (modsJson != null) - return modsJson; + if (localAPIMods != null) + return localAPIMods; if (mods == null) - return null; + return Array.Empty(); - return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym })); + return localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); } set { - modsJson = value; + localAPIMods = value; - // we potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. + // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. mods = null; } } + // Used for database serialisation/deserialisation. + [Column("Mods")] + private string modsString + { + get => JsonConvert.SerializeObject(apiMods); + set => apiMods = JsonConvert.DeserializeObject(value); + } + [NotMapped] [JsonProperty("user")] public User User { get; set; } @@ -251,14 +258,6 @@ namespace osu.Game.Scoring } } - [Serializable] - protected class DeserializedMod : IMod - { - public string Acronym { get; set; } - - public bool Equals(IMod other) => Acronym == other?.Acronym; - } - public override string ToString() => $"{User} playing {Beatmap}"; public bool Equals(ScoreInfo other) From 982d8fa8b1ef8ce69a4456b8a31192f948314f09 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 20:49:26 +0900 Subject: [PATCH 31/65] Fix incorrect reference --- osu.Game/Scoring/ScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index df8f309ee0..1e438edeea 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -68,7 +68,7 @@ namespace osu.Game.Scoring if (mods != null) return mods; - if (apiMods == null) + if (localAPIMods == null) return Array.Empty(); var rulesetInstance = Ruleset.CreateInstance(); From 625484468e4ba788e04b73b0c6b55614d8e30bdf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 20:49:37 +0900 Subject: [PATCH 32/65] Fix DB serialisation --- osu.Game/Scoring/ScoreInfo.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 1e438edeea..01b62f8b4f 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -105,8 +105,9 @@ namespace osu.Game.Scoring } // Used for database serialisation/deserialisation. + [JsonIgnore] [Column("Mods")] - private string modsString + public string ModsString { get => JsonConvert.SerializeObject(apiMods); set => apiMods = JsonConvert.DeserializeObject(value); From 8413b0a5d32a7aa159bfc971f10b2b11bd058c5c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 20:49:44 +0900 Subject: [PATCH 33/65] Don't map api mods to DB --- osu.Game/Scoring/ScoreInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 01b62f8b4f..312b5f46f4 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -83,6 +83,7 @@ namespace osu.Game.Scoring // Used for API serialisation/deserialisation. [JsonProperty("mods")] + [NotMapped] private APIMod[] apiMods { get From e9a114a15c37bf498ff7e1e8b24775964f01a4b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 20:50:18 +0900 Subject: [PATCH 34/65] Rename property back --- osu.Game/Scoring/ScoreInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 312b5f46f4..222f69b025 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -108,7 +108,7 @@ namespace osu.Game.Scoring // Used for database serialisation/deserialisation. [JsonIgnore] [Column("Mods")] - public string ModsString + public string ModsJson { get => JsonConvert.SerializeObject(apiMods); set => apiMods = JsonConvert.DeserializeObject(value); From c531e38a369452f3be8664e0c2b6ff4bb079e64c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 22:00:27 +0900 Subject: [PATCH 35/65] Rework to create a derived tracked user data instead --- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- .../MultiplayerSpectatorLeaderboard.cs | 85 +++++------ .../HUD/MultiplayerGameplayLeaderboard.cs | 137 +++++++++--------- 3 files changed, 107 insertions(+), 117 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 1ee848b902..272d547505 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.Multiplayer break; } - ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty())); + ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) })); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs index aa9f162036..1b9e2bda2d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectatorLeaderboard.cs @@ -2,10 +2,8 @@ // 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.Timing; -using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -13,73 +11,62 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard { - private readonly Dictionary trackedData = new Dictionary(); - public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) : base(scoreProcessor, userIds) { } - public void AddClock(int userId, IClock source) => trackedData[userId] = new TrackedUserData(source); - - public void RemoveClock(int userId) => trackedData.Remove(userId); - - protected override void OnIncomingFrames(int userId, FrameDataBundle bundle) + public void AddClock(int userId, IClock clock) { - if (!trackedData.TryGetValue(userId, out var data)) + if (!UserScores.TryGetValue(userId, out var data)) return; - data.Frames.Add(new TimedFrameHeader(bundle.Frames.First().Time, bundle.Header)); + ((SpectatingTrackedUserData)data).Clock = clock; } + public void RemoveClock(int userId) + { + if (!UserScores.TryGetValue(userId, out var data)) + return; + + ((SpectatingTrackedUserData)data).Clock = null; + } + + protected override TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(userId, scoreProcessor); + protected override void Update() { base.Update(); - foreach (var (userId, data) in trackedData) + foreach (var (_, data) in UserScores) + data.UpdateScore(); + } + + private class SpectatingTrackedUserData : TrackedUserData + { + [CanBeNull] + public IClock Clock; + + public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor) + : base(userId, scoreProcessor) { - var targetTime = data.Clock.CurrentTime; + } - if (data.Frames.Count == 0) - continue; + public override void UpdateScore() + { + if (Frames.Count == 0) + return; - int frameIndex = data.Frames.BinarySearch(new TimedFrameHeader(targetTime)); + if (Clock == null) + return; + + int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime)); if (frameIndex < 0) frameIndex = ~frameIndex; - frameIndex = Math.Clamp(frameIndex - 1, 0, data.Frames.Count - 1); + frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1); - SetCurrentFrame(userId, data.Frames[frameIndex].Header); + SetFrame(Frames[frameIndex]); } } - - private class TrackedUserData - { - public readonly IClock Clock; - public readonly List Frames = new List(); - - public TrackedUserData(IClock clock) - { - Clock = clock; - } - } - - private class TimedFrameHeader : IComparable - { - public readonly double Time; - public readonly FrameHeader Header; - - public TimedFrameHeader(double time) - { - Time = time; - } - - public TimedFrameHeader(double time, FrameHeader header) - { - Time = time; - Header = header; - } - - public int CompareTo(TimedFrameHeader other) => Time.CompareTo(other.Time); - } } } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index f0d2ac4b7f..70de067784 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.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; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; @@ -19,8 +19,7 @@ namespace osu.Game.Screens.Play.HUD [LongRunningLoad] public class MultiplayerGameplayLeaderboard : GameplayLeaderboard { - private readonly ScoreProcessor scoreProcessor; - private readonly Dictionary userScores = new Dictionary(); + protected readonly Dictionary UserScores = new Dictionary(); [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -31,9 +30,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private UserLookupCache userLookupCache { get; set; } - private Bindable scoringMode; - + private readonly ScoreProcessor scoreProcessor; private readonly BindableList playingUsers; + private Bindable scoringMode; /// /// Construct a new leaderboard. @@ -52,6 +51,8 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { + scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + foreach (var userId in playingUsers) { streamingClient.WatchUser(userId); @@ -59,19 +60,17 @@ namespace osu.Game.Screens.Play.HUD // probably won't be required in the final implementation. var resolvedUser = userLookupCache.GetUserAsync(userId).Result; - var trackedUser = new TrackedUserData(); + var trackedUser = CreateUserData(userId, scoreProcessor); + trackedUser.ScoringMode.BindTo(scoringMode); - userScores[userId] = trackedUser; var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id); + leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); + leaderboardScore.TotalScore.BindTo(trackedUser.Score); + leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); + leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); - ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); - ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); - ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo); - ((IBindable)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit); + UserScores[userId] = trackedUser; } - - scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); - scoringMode.BindValueChanged(updateAllScores, true); } protected override void LoadComplete() @@ -101,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD { streamingClient.StopWatchingUser(userId); - if (userScores.TryGetValue(userId, out var trackedData)) + if (UserScores.TryGetValue(userId, out var trackedData)) trackedData.MarkUserQuit(); } @@ -109,39 +108,16 @@ namespace osu.Game.Screens.Play.HUD } } - private void updateAllScores(ValueChangedEvent mode) - { - foreach (var trackedData in userScores.Values) - trackedData.UpdateScore(scoreProcessor, mode.NewValue); - } - private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() => { - if (userScores.ContainsKey(userId)) - OnIncomingFrames(userId, bundle); + if (!UserScores.TryGetValue(userId, out var trackedData)) + return; + + trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); + trackedData.UpdateScore(); }); - /// - /// Invoked when new frames have arrived for a user. - /// - /// - /// By default, this immediately sets the current frame to be displayed for the user. - /// - /// The user which the frames arrived for. - /// The bundle of frames. - protected virtual void OnIncomingFrames(int userId, FrameDataBundle bundle) => SetCurrentFrame(userId, bundle.Header); - - /// - /// Sets the current frame to be displayed for a user. - /// - /// The user to set the frame of. - /// The frame to set. - protected void SetCurrentFrame(int userId, FrameHeader header) - { - var trackedScore = userScores[userId]; - trackedScore.LastHeader = header; - trackedScore.UpdateScore(scoreProcessor, scoringMode.Value); - } + protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor); protected override void Dispose(bool isDisposing) { @@ -158,38 +134,65 @@ namespace osu.Game.Screens.Play.HUD } } - private class TrackedUserData + protected class TrackedUserData { - public IBindableNumber Score => score; + public readonly int UserId; + public readonly ScoreProcessor ScoreProcessor; - private readonly BindableDouble score = new BindableDouble(); + public readonly BindableDouble Score = new BindableDouble(); + public readonly BindableDouble Accuracy = new BindableDouble(1); + public readonly BindableInt CurrentCombo = new BindableInt(); + public readonly BindableBool UserQuit = new BindableBool(); - public IBindableNumber Accuracy => accuracy; + public readonly IBindable ScoringMode = new Bindable(); - private readonly BindableDouble accuracy = new BindableDouble(1); + public readonly List Frames = new List(); - public IBindableNumber CurrentCombo => currentCombo; - - 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) + public TrackedUserData(int userId, ScoreProcessor scoreProcessor) { - if (LastHeader == null) + UserId = userId; + ScoreProcessor = scoreProcessor; + + ScoringMode.BindValueChanged(_ => UpdateScore()); + } + + public void MarkUserQuit() => UserQuit.Value = true; + + public virtual void UpdateScore() + { + if (Frames.Count == 0) return; - score.Value = processor.GetImmediateScore(mode, LastHeader.MaxCombo, LastHeader.Statistics); - accuracy.Value = LastHeader.Accuracy; - currentCombo.Value = LastHeader.Combo; + SetFrame(Frames.Last()); } + + protected void SetFrame(TimedFrame frame) + { + var header = frame.Header; + + Score.Value = ScoreProcessor.GetImmediateScore(ScoringMode.Value, header.MaxCombo, header.Statistics); + Accuracy.Value = header.Accuracy; + CurrentCombo.Value = header.Combo; + } + } + + protected class TimedFrame : IComparable + { + public readonly double Time; + public readonly FrameHeader Header; + + public TimedFrame(double time) + { + Time = time; + } + + public TimedFrame(double time, FrameHeader header) + { + Time = time; + Header = header; + } + + public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time); } } } From 1e002841cff4ffeac18a58c57c6a0769e33b05c9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Apr 2021 22:03:45 +0900 Subject: [PATCH 36/65] Add test for scoring mode changes --- .../TestSceneMultiplayerGameplayLeaderboard.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 272d547505..b6c06bb149 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online; using osu.Game.Online.API; @@ -38,6 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + private OsuConfigManager config; + public TestSceneMultiplayerGameplayLeaderboard() { base.Content.Children = new Drawable[] @@ -48,6 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }; } + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); + } + [SetUpSteps] public override void SetUpSteps() { @@ -97,6 +106,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users); } + [Test] + public void TestChangeScoringMode() + { + AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5); + AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic)); + AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); + } + public class TestMultiplayerStreaming : SpectatorStreamingClient { public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; From d9e3276d0edb2f163efd5dcd5894a2881b89ab78 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 12 Apr 2021 19:18:22 +0200 Subject: [PATCH 37/65] Don't update path type once immediately --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 3 ++- 1 file changed, 2 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 6b78cff33e..7686043c43 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -58,12 +58,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { this.slider = slider; ControlPoint = controlPoint; + PointsInSegment = slider.Path.PointsInSegment(ControlPoint); slider.Path.Version.BindValueChanged(_ => { PointsInSegment = slider.Path.PointsInSegment(ControlPoint); updatePathType(); - }, runOnceImmediately: true); + }); controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); From 92fab653e175b594327cbce5a7702a6068cf6a58 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 12 Apr 2021 20:10:22 +0300 Subject: [PATCH 38/65] Take current mod settings value into account on equality comparsion --- osu.Game/Online/API/APIMod.cs | 26 +++++++++++++++++++++++++- osu.Game/Rulesets/Mods/Mod.cs | 14 +++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index bad7e0af38..4427c82a8b 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Utils; namespace osu.Game.Online.API { @@ -64,7 +65,15 @@ namespace osu.Game.Online.API } public bool Equals(IMod other) => other is APIMod them && Equals(them); - public bool Equals(APIMod other) => Acronym == other?.Acronym; + + public bool Equals(APIMod other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Acronym == other.Acronym && + Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default); + } public override string ToString() { @@ -73,5 +82,20 @@ namespace osu.Game.Online.API return $"{Acronym}"; } + + private class ModSettingsEqualityComparer : IEqualityComparer> + { + public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer(); + + public bool Equals(KeyValuePair x, KeyValuePair y) + { + object xValue = ModUtils.GetSettingUnderlyingValue(x.Value); + object yValue = ModUtils.GetSettingUnderlyingValue(y.Value); + + return x.Key == y.Key && EqualityComparer.Default.Equals(xValue, yValue); + } + + public int GetHashCode(KeyValuePair obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value)); + } } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 93a11e70f8..832a14ee1e 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; +using osu.Game.Utils; namespace osu.Game.Rulesets.Mods { @@ -173,7 +174,18 @@ namespace osu.Game.Rulesets.Mods } public bool Equals(IMod other) => other is Mod them && Equals(them); - public bool Equals(Mod other) => GetType() == other?.GetType(); + + public bool Equals(Mod other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return GetType() == other.GetType() && + this.GetSettingsSourceProperties().All(pair => + EqualityComparer.Default.Equals( + ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(this)), + ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(other)))); + } /// /// Reset all custom settings for this mod back to their defaults. From 589e1a2a471ef69c49d24d42d5b56df9362f7b6c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 12 Apr 2021 20:51:24 +0300 Subject: [PATCH 39/65] Add mod settings equality test --- .../Mods/ModSettingsEqualityComparsion.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 osu.Game.Tests/Mods/ModSettingsEqualityComparsion.cs diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparsion.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparsion.cs new file mode 100644 index 0000000000..7a5789f01a --- /dev/null +++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparsion.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Online.API; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Tests.Mods +{ + [TestFixture] + public class ModSettingsEqualityComparison + { + [Test] + public void Test() + { + var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } }; + var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }; + var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }; + var apiMod1 = new APIMod(mod1); + var apiMod2 = new APIMod(mod2); + var apiMod3 = new APIMod(mod3); + + Assert.That(mod1, Is.Not.EqualTo(mod2)); + Assert.That(apiMod1, Is.Not.EqualTo(apiMod2)); + + Assert.That(mod2, Is.EqualTo(mod2)); + Assert.That(apiMod2, Is.EqualTo(apiMod2)); + + Assert.That(mod2, Is.EqualTo(mod3)); + Assert.That(apiMod2, Is.EqualTo(apiMod3)); + + Assert.That(mod3, Is.EqualTo(mod2)); + Assert.That(apiMod3, Is.EqualTo(apiMod2)); + } + } +} From 8f84abf34807bae9679b2f70cdfd8c082eb25e03 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 12 Apr 2021 21:51:04 +0300 Subject: [PATCH 40/65] Display "replays watched" tooltip for replays subsection --- .../Visual/Online/TestSceneUserHistoryGraph.cs | 3 +-- .../Sections/Historical/ChartProfileSubsection.cs | 7 ++++++- .../Sections/Historical/PlayHistorySubsection.cs | 2 ++ .../Profile/Sections/Historical/ProfileLineChart.cs | 4 ++-- .../Profile/Sections/Historical/ReplaysSubsection.cs | 2 ++ .../Profile/Sections/Historical/UserHistoryGraph.cs | 12 +++++++----- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs index 57ce4c41e7..484c59695e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs @@ -19,13 +19,12 @@ namespace osu.Game.Tests.Visual.Online { UserHistoryGraph graph; - Add(graph = new UserHistoryGraph + Add(graph = new UserHistoryGraph("Test") { RelativeSizeAxes = Axes.X, Height = 200, Anchor = Anchor.Centre, Origin = Anchor.Centre, - TooltipCounterName = "Test" }); var values = new[] diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs index b82773155d..a48036dcbb 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -15,6 +15,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { private ProfileLineChart chart; + /// + /// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the history graph tooltip. + /// + protected abstract string GraphCounterName { get; } + protected ChartProfileSubsection(Bindable user, string headerText) : base(user, headerText) { @@ -30,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Left = 20, Right = 40 }, - Child = chart = new ProfileLineChart() + Child = chart = new ProfileLineChart(GraphCounterName) }; protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs index 2f15886c3a..dfd29db693 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs @@ -9,6 +9,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { public class PlayHistorySubsection : ChartProfileSubsection { + protected override string GraphCounterName => "Plays"; + public PlayHistorySubsection(Bindable user) : base(user, "Play History") { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index f02aa36b6c..eb5deb2802 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private readonly Container rowLinesContainer; private readonly Container columnLinesContainer; - public ProfileLineChart() + public ProfileLineChart(string graphCounterName) { RelativeSizeAxes = Axes.X; Height = 250; @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } }, - graph = new UserHistoryGraph + graph = new UserHistoryGraph(graphCounterName) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs index e594e8d020..1c28306f17 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs @@ -9,6 +9,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { public class ReplaysSubsection : ChartProfileSubsection { + protected override string GraphCounterName => "Replays Watched"; + public ReplaysSubsection(Bindable user) : base(user, "Replays Watched History") { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index b1e8c8f0ca..f9e5ccf618 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -11,20 +11,22 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { public class UserHistoryGraph : UserGraph { + private readonly string tooltipCounterName; + [CanBeNull] 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"; + public UserHistoryGraph(string tooltipCounterName) + { + this.tooltipCounterName = tooltipCounterName; + } protected override float GetDataPointHeight(long playCount) => playCount; - protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(TooltipCounterName); + protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(tooltipCounterName); protected override object GetTooltipContent(DateTime date, long playCount) { From 17c2c4e885b63761fa179ddc2ee193b6078a4b03 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 13 Apr 2021 05:31:56 +0300 Subject: [PATCH 41/65] Fix test case filename not matching --- ...ingsEqualityComparsion.cs => ModSettingsEqualityComparison.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Tests/Mods/{ModSettingsEqualityComparsion.cs => ModSettingsEqualityComparison.cs} (100%) diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparsion.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs similarity index 100% rename from osu.Game.Tests/Mods/ModSettingsEqualityComparsion.cs rename to osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs From 66e74da2b74a10ed695b073bf1a92c5c1b7b7d32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 13:03:14 +0900 Subject: [PATCH 42/65] Fix regression in quick delete mouse action blocking --- .../Visual/Editing/TestSceneEditorQuickDelete.cs | 3 ++- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 8 ++++---- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index 8a0f27b851..25e12b7a88 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -83,8 +83,9 @@ namespace osu.Game.Tests.Visual.Editing 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)); + + // second click should nuke the object completely. AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 64cf0e7512..b5a28dc022 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - bool selectionPerformed = beginClickSelection(e); + bool selectionPerformed = performMouseDownActions(e); // even if a selection didn't occur, a drag event may still move the selection. prepareSelectionMovement(); @@ -343,7 +343,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The input event that triggered this selection. /// Whether a selection was performed. - private bool beginClickSelection(MouseButtonEvent e) + private bool performMouseDownActions(MouseButtonEvent e) { // Iterate from the top of the input stack (blueprints closest to the front of the screen first). // Priority is given to already-selected blueprints. @@ -351,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (!blueprint.IsHovered) continue; - return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e); + return clickSelectionBegan = SelectionHandler.MouseDownSelectionRequested(blueprint, e); } return false; @@ -375,7 +375,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (!blueprint.IsHovered) continue; - return clickSelectionBegan = SelectionHandler.HandleDeselectionRequested(blueprint, e); + return clickSelectionBegan = SelectionHandler.MouseUpSelectionRequested(blueprint, e); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index e5e1100797..389ef78ed5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -220,12 +220,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. /// The mouse event responsible for selection. /// Whether a selection was performed. - internal bool HandleSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) + internal bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (e.ShiftPressed && e.Button == MouseButton.Right) { handleQuickDeletion(blueprint); - return false; + return true; } // while holding control, we only want to add to selection, not replace an existing selection. @@ -244,7 +244,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. /// The mouse event responsible for deselection. /// Whether a deselection was performed. - internal bool HandleDeselectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) + internal bool MouseUpSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (blueprint.IsSelected) { From 05d7fe289f3e6a48b855edc629e163fe7ea26fff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 13:09:18 +0900 Subject: [PATCH 43/65] Rename test scene in preparation for increasing scope --- ...estSceneEditorQuickDelete.cs => TestSceneEditorSelection.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Editing/{TestSceneEditorQuickDelete.cs => TestSceneEditorSelection.cs} (98%) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs similarity index 98% rename from osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs rename to osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs index 25e12b7a88..36c4357432 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs @@ -18,7 +18,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { - public class TestSceneEditorQuickDelete : EditorTestScene + public class TestSceneEditorSelection : EditorTestScene { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); From 7c975359d9b6b06a797a7121c38700e09c2f08b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 13:29:37 +0900 Subject: [PATCH 44/65] Add basic select/deselect tests --- .../Editing/TestSceneEditorSelection.cs | 98 ++++++++++++++++++- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs index 36c4357432..4f386972fa 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs @@ -27,6 +27,97 @@ namespace osu.Game.Tests.Visual.Editing private BlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); + private void moveMouseToObject(HitObject obj) + { + AddStep("move mouse to object", () => + { + var pos = blueprintContainer.SelectionBlueprints + .First(s => s.HitObject == obj) + .ChildrenOfType() + .First().ScreenSpaceDrawQuad.Centre; + + InputManager.MoveMouseTo(pos); + }); + } + + [Test] + public void TestBasicSelect() + { + var addedObject = new HitCircle { StartTime = 100 }; + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + moveMouseToObject(addedObject); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + var addedObject2 = new HitCircle + { + StartTime = 100, + Position = new Vector2(100), + }; + + AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2)); + AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + moveMouseToObject(addedObject2); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2); + } + + [Test] + public void TestMultiSelect() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(50) }, + new HitCircle { StartTime = 300, Position = new Vector2(100) }, + new HitCircle { StartTime = 400, Position = new Vector2(150) }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + moveMouseToObject(addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]); + + AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft)); + + moveMouseToObject(addedObjects[1]); + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); + + moveMouseToObject(addedObjects[2]); + AddStep("click third", () => InputManager.Click(MouseButton.Left)); + AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2])); + + moveMouseToObject(addedObjects[1]); + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); + } + + [Test] + public void TestBasicDeselect() + { + var addedObject = new HitCircle { StartTime = 100 }; + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + moveMouseToObject(addedObject); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + AddStep("click away", () => + { + InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("selection lost", () => EditorBeatmap.SelectedHitObjects.Count == 0); + } + [Test] public void TestQuickDeleteRemovesObject() { @@ -36,11 +127,8 @@ namespace osu.Game.Tests.Visual.Editing AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); - AddStep("move mouse to object", () => - { - var pos = blueprintContainer.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre; - InputManager.MoveMouseTo(pos); - }); + moveMouseToObject(addedObject); + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); AddStep("right click", () => InputManager.Click(MouseButton.Right)); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); From 516bd138e32ba1bd3e4ae35a150d4ae1e9b0d8ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 13:46:38 +0900 Subject: [PATCH 45/65] Add (previously failing) test coverage of drag from selection --- .../Editing/TestSceneEditorSelection.cs | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs index 4f386972fa..99f31b0c2a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -27,12 +28,12 @@ namespace osu.Game.Tests.Visual.Editing private BlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); - private void moveMouseToObject(HitObject obj) + private void moveMouseToObject(Func targetFunc) { AddStep("move mouse to object", () => { var pos = blueprintContainer.SelectionBlueprints - .First(s => s.HitObject == obj) + .First(s => s.HitObject == targetFunc()) .ChildrenOfType() .First().ScreenSpaceDrawQuad.Centre; @@ -46,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing var addedObject = new HitCircle { StartTime = 100 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); - moveMouseToObject(addedObject); + moveMouseToObject(() => addedObject); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); @@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2)); AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); - moveMouseToObject(addedObject2); + moveMouseToObject(() => addedObject2); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2); } @@ -78,33 +79,69 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); - moveMouseToObject(addedObjects[0]); + moveMouseToObject(() => addedObjects[0]); AddStep("click first", () => InputManager.Click(MouseButton.Left)); AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]); AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft)); - moveMouseToObject(addedObjects[1]); + moveMouseToObject(() => addedObjects[1]); AddStep("click second", () => InputManager.Click(MouseButton.Left)); AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); - moveMouseToObject(addedObjects[2]); + moveMouseToObject(() => addedObjects[2]); AddStep("click third", () => InputManager.Click(MouseButton.Left)); AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2])); - moveMouseToObject(addedObjects[1]); + moveMouseToObject(() => addedObjects[1]); AddStep("click second", () => InputManager.Click(MouseButton.Left)); AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); } + [TestCase(false)] + [TestCase(true)] + public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) + { + HitCircle[] addedObjects = null; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(50) }, + new HitCircle { StartTime = 300, Position = new Vector2(100) }, + new HitCircle { StartTime = 400, Position = new Vector2(150) }, + })); + + moveMouseToObject(() => addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + + AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft)); + + moveMouseToObject(() => addedObjects[1]); + + if (alreadySelectedBeforeDrag) + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + + AddStep("mouse down on second", () => InputManager.PressButton(MouseButton.Left)); + + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); + + AddStep("drag to centre", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre)); + + AddAssert("positions changed", () => addedObjects[0].Position != Vector2.Zero && addedObjects[1].Position != new Vector2(50)); + + AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft)); + AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left)); + } + [Test] public void TestBasicDeselect() { var addedObject = new HitCircle { StartTime = 100 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); - moveMouseToObject(addedObject); + moveMouseToObject(() => addedObject); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); @@ -127,7 +164,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); - moveMouseToObject(addedObject); + moveMouseToObject(() => addedObject); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); AddStep("right click", () => InputManager.Click(MouseButton.Right)); From a664efe12bc4e0fb38d71a43b25e59335cb8c7c1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 13 Apr 2021 07:59:13 +0300 Subject: [PATCH 46/65] Fix history graph tooltips leaking to others Since there was no check about which tooltip content came from which graph, all history graphs use the "Replays Watched" tooltip, as it is the latest created one. --- .../Profile/Sections/Historical/UserHistoryGraph.cs | 7 ++++++- osu.Game/Overlays/Profile/UserGraph.cs | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index f9e5ccf618..52831b4243 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -32,6 +32,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { return new TooltipDisplayContent { + Name = tooltipCounterName, Count = playCount.ToString("N0"), Date = date.ToString("MMMM yyyy") }; @@ -39,14 +40,17 @@ namespace osu.Game.Overlays.Profile.Sections.Historical protected class HistoryGraphTooltip : UserGraphTooltip { + private readonly string tooltipCounterName; + public HistoryGraphTooltip(string tooltipCounterName) : base(tooltipCounterName) { + this.tooltipCounterName = tooltipCounterName; } public override bool SetContent(object content) { - if (!(content is TooltipDisplayContent info)) + if (!(content is TooltipDisplayContent info) || info.Name != tooltipCounterName) return false; Counter.Text = info.Count; @@ -57,6 +61,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private class TooltipDisplayContent { + public string Name; public string Count; public string Date; } diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index cdfd722d68..e2eb5d5180 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -208,6 +208,7 @@ namespace osu.Game.Overlays.Profile protected abstract class UserGraphTooltip : VisibilityContainer, ITooltip { + protected new readonly OsuSpriteText Name; protected readonly OsuSpriteText Counter, BottomText; private readonly Box background; @@ -237,7 +238,7 @@ namespace osu.Game.Overlays.Profile Spacing = new Vector2(3, 0), Children = new Drawable[] { - new OsuSpriteText + Name = new OsuSpriteText { Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), Text = tooltipCounterName From 4852630c93564dd7af90ab18ea67c55b416e2655 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 14:03:42 +0900 Subject: [PATCH 47/65] Fix import multiple file types via drag potentially reaching the wrong importer --- osu.Game/OsuGameBase.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ca132df552..e285faab11 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -433,12 +433,15 @@ namespace osu.Game if (paths.Length == 0) return; - var extension = Path.GetExtension(paths.First())?.ToLowerInvariant(); + var filesPerExtension = paths.GroupBy(p => Path.GetExtension(p).ToLowerInvariant()); - foreach (var importer in fileImporters) + foreach (var groups in filesPerExtension) { - if (importer.HandledExtensions.Contains(extension)) - await importer.Import(paths).ConfigureAwait(false); + foreach (var importer in fileImporters) + { + if (importer.HandledExtensions.Contains(groups.Key)) + await importer.Import(groups.ToArray()).ConfigureAwait(false); + } } } From 273099d53c6b0dfab4a694055fc12bf45d5611f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 14:31:41 +0900 Subject: [PATCH 48/65] Don't store online IDs from score submission responses for now Closes remaining portion of https://github.com/ppy/osu/issues/12372. --- osu.Game/Screens/Play/SubmittingPlayer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index d22199447d..b64d835c58 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -109,7 +109,12 @@ namespace osu.Game.Screens.Play request.Success += s => { - score.ScoreInfo.OnlineScoreID = s.ID; + // For the time being, online ID responses are not really useful for anything. + // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores. + // + // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint + // conflicts across various systems (ie. solo and multiplayer). + // score.ScoreInfo.OnlineScoreID = s.ID; tcs.SetResult(true); }; From 9f8af03a70633d32fa11842c69ea73b1ccf72f11 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 13 Apr 2021 09:28:58 +0300 Subject: [PATCH 49/65] Remove irrelevant change --- osu.Game/Overlays/Profile/UserGraph.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index e2eb5d5180..cdfd722d68 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -208,7 +208,6 @@ namespace osu.Game.Overlays.Profile protected abstract class UserGraphTooltip : VisibilityContainer, ITooltip { - protected new readonly OsuSpriteText Name; protected readonly OsuSpriteText Counter, BottomText; private readonly Box background; @@ -238,7 +237,7 @@ namespace osu.Game.Overlays.Profile Spacing = new Vector2(3, 0), Children = new Drawable[] { - Name = new OsuSpriteText + new OsuSpriteText { Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), Text = tooltipCounterName From fbc6fb8fc55ad431173ad61072a84e26629469e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 15:35:57 +0900 Subject: [PATCH 50/65] Split out common logic into private method and add inline comment for future visitors --- .../Sliders/Components/PathControlPointPiece.cs | 8 ++++++-- 1 file changed, 6 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 7686043c43..ce9580d0f4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -58,11 +58,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { this.slider = slider; ControlPoint = controlPoint; - PointsInSegment = slider.Path.PointsInSegment(ControlPoint); + + // we don't want to run the path type update on construction as it may inadvertently change the slider. + cachePoints(slider); slider.Path.Version.BindValueChanged(_ => { - PointsInSegment = slider.Path.PointsInSegment(ControlPoint); + cachePoints(slider); updatePathType(); }); @@ -206,6 +208,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); + private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint); + /// /// Handles correction of invalid path types. /// From a9652b7b259b2921648c220e1d7f2ccf98276afa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 16:05:12 +0900 Subject: [PATCH 51/65] Start TimelineTestScene in a more visible place --- osu.Game.Tests/Visual/Editing/TimelineTestScene.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 1da6433707..88b4614791 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -64,6 +64,13 @@ namespace osu.Game.Tests.Visual.Editing }); } + protected override void LoadComplete() + { + base.LoadComplete(); + + Clock.Seek(2500); + } + public abstract Drawable CreateTestComponent(); private class AudioVisualiser : CompositeDrawable From ebf97ff48ff43a5879b19fcb9365c84c8369fe21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 16:25:43 +0900 Subject: [PATCH 52/65] Update timeline ticks to use width as a differentiation method, rather than height --- .../Visualisations/PointVisualisation.cs | 10 ++-- .../Timeline/TimelineTickDisplay.cs | 56 +++++++++++-------- 2 files changed, 39 insertions(+), 27 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 b0ecffdd24..b5c4cd6dda 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osuTK; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations { @@ -12,7 +11,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations /// public class PointVisualisation : Box { - public const float WIDTH = 1; + public const float MAX_WIDTH = 5; public PointVisualisation(double startTime) : this() @@ -27,8 +26,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Y; - Width = WIDTH; - EdgeSmoothness = new Vector2(WIDTH, 0); + Anchor = Anchor.CentreLeft; + Origin = Anchor.Centre; + + Width = MAX_WIDTH; + Height = 0.75f; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index c070c833f8..7ec5bb7197 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -6,9 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -33,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private OsuColour colours { get; set; } + private static readonly int highest_divisor = BindableBeatDivisor.VALID_DIVISORS.Last(); + public TimelineTickDisplay() { RelativeSizeAxes = Axes.Both; @@ -80,8 +80,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (timeline != null) { var newRange = ( - (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X, - (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X); + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X, + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X); if (visibleRange != newRange) { @@ -100,7 +100,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void createTicks() { int drawableIndex = 0; - int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last(); nextMinTick = null; nextMaxTick = null; @@ -131,25 +130,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); var colour = BindableBeatDivisor.GetColourFor(divisor, colours); - bool isMainBeat = indexInBar == 0; - // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn. - float height = isMainBeat ? 0.5f : 0.4f - (float)divisor / highestDivisor * 0.2f; - float gradientOpacity = isMainBeat ? 1 : 0; - var topPoint = getNextUsablePoint(); - topPoint.X = xPos; - topPoint.Height = height; - topPoint.Colour = ColourInfo.GradientVertical(colour, colour.Opacity(gradientOpacity)); - topPoint.Anchor = Anchor.TopLeft; - topPoint.Origin = Anchor.TopCentre; - - var bottomPoint = getNextUsablePoint(); - bottomPoint.X = xPos; - bottomPoint.Anchor = Anchor.BottomLeft; - bottomPoint.Colour = ColourInfo.GradientVertical(colour.Opacity(gradientOpacity), colour); - bottomPoint.Origin = Anchor.BottomCentre; - bottomPoint.Height = height; + var line = getNextUsableLine(); + line.X = xPos; + line.Width = PointVisualisation.MAX_WIDTH * getWidth(indexInBar, divisor); + line.Colour = colour; } beat++; @@ -168,7 +154,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline tickCache.Validate(); - Drawable getNextUsablePoint() + Drawable getNextUsableLine() { PointVisualisation point; if (drawableIndex >= Count) @@ -183,6 +169,30 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } + private static float getWidth(int indexInBar, int divisor) + { + if (indexInBar == 0) + return 1; + + switch (divisor) + { + case 1: + case 2: + return 0.6f; + + case 3: + case 4: + return 0.5f; + + case 6: + case 8: + return 0.4f; + + default: + return 0.3f; + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 27e851c2eed2b0edc1833a620e7ec4e9e85973f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 16:41:29 +0900 Subject: [PATCH 53/65] Also adjust height --- .../Visualisations/PointVisualisation.cs | 2 +- .../Timeline/TimelineTickDisplay.cs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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 b5c4cd6dda..53a1f94731 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations /// public class PointVisualisation : Box { - public const float MAX_WIDTH = 5; + public const float MAX_WIDTH = 4; public PointVisualisation(double startTime) : this() diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 7ec5bb7197..3aaf0451c8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -135,6 +135,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var line = getNextUsableLine(); line.X = xPos; line.Width = PointVisualisation.MAX_WIDTH * getWidth(indexInBar, divisor); + line.Height = 0.9f * getHeight(indexInBar, divisor); line.Colour = colour; } @@ -193,6 +194,30 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } + private static float getHeight(int indexInBar, int divisor) + { + if (indexInBar == 0) + return 1; + + switch (divisor) + { + case 1: + case 2: + return 0.9f; + + case 3: + case 4: + return 0.8f; + + case 6: + case 8: + return 0.7f; + + default: + return 0.6f; + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 5a06db8a113dfd012a46b304f9967508f0ca609e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 16:48:05 +0900 Subject: [PATCH 54/65] Change default editor waveform opacity to 25% The previous setting felt way too high. --- 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 387cfbb193..0525e84077 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -143,7 +143,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full); - SetDefault(OsuSetting.EditorWaveformOpacity, 1f); + SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f); } public OsuConfigManager(Storage storage) From 0932daeaa8564fe9fd9e25560abe1bfa5a8b20b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 16:50:03 +0900 Subject: [PATCH 55/65] Force the new default on update --- osu.Game/Configuration/OsuConfigManager.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 0525e84077..21a1a1d430 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -169,14 +169,9 @@ namespace osu.Game.Configuration int combined = (year * 10000) + monthDay; - if (combined < 20200305) + if (combined < 20210413) { - // the maximum value of this setting was changed. - // if we don't manually increase this, it causes song select to filter out beatmaps the user expects to see. - var maxStars = (BindableDouble)GetOriginalBindable(OsuSetting.DisplayStarsMaximum); - - if (maxStars.Value == 10) - maxStars.Value = maxStars.MaxValue; + SetValue(OsuSetting.EditorWaveformOpacity, 0.25f); } } From b41e3a2e7a84046981007ebc6a6d2ecd85d1b42b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 17:38:13 +0900 Subject: [PATCH 56/65] Remove unused using statement --- osu.Game/Configuration/OsuConfigManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 21a1a1d430..f9b1c9618b 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; From 4618728bf016fc3253d0cce337c230a2ed0f2fac Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Tue, 13 Apr 2021 11:35:12 +0200 Subject: [PATCH 57/65] Add test case --- .../TestSceneOsuEditorSelectInvalidPath.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs new file mode 100644 index 0000000000..d0348c1b6b --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.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.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneOsuEditorSelectInvalidPath : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [Test] + public void TestSelectDoesNotModify() + { + Slider slider = new Slider { StartTime = 0, Position = new Vector2(320, 40) }; + + PathControlPoint[] points = + { + new PathControlPoint(new Vector2(0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(-100, 0)), + new PathControlPoint(new Vector2(100, 20)) + }; + + int preSelectVersion = -1; + AddStep("add slider", () => + { + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + preSelectVersion = slider.Path.Version.Value; + }); + + AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); + + AddAssert("slider same path", () => slider.Path.Version.Value == preSelectVersion); + } + } +} From fca9c70c1b47bd986fe089e675f85ea44d9b6690 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 17:39:12 +0900 Subject: [PATCH 58/65] Move timeline hit object test to immediately viewable area --- .../Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 35f394fe1d..88246381bf 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Editing EditorBeatmap.Add(new Spinner { Position = new Vector2(256, 256), - StartTime = 150, + StartTime = 2700, Duration = 500 }); }); From 00f235760d237e6902162c4f1675cbedbc0063ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 18:12:46 +0900 Subject: [PATCH 59/65] Update visual appearance of timeline blueprints to close match new designs --- .../TestSceneTimelineHitObjectBlueprint.cs | 6 +- .../Timeline/TimelineBlueprintContainer.cs | 3 +- .../Timeline/TimelineHitObjectBlueprint.cs | 166 +++++++++--------- 3 files changed, 92 insertions(+), 83 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 88246381bf..e6fad33a51 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestDisallowZeroDurationObjects() { - DragBar dragBar; + DragArea dragArea; AddStep("add spinner", () => { @@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Editing 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); + dragArea = this.ChildrenOfType().Single(bar => bar.HandlePositionalInput); + InputManager.MoveMouseTo(dragArea); InputManager.PressButton(MouseButton.Left); }); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index be34c8d57e..7427473a35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -63,7 +63,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { AddInternal(backgroundBox = new SelectableAreaBackground { - Colour = Color4.Black + Colour = Color4.Black, + Depth = float.MaxValue, }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index d24614299c..179abd0a26 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using JetBrains.Annotations; 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; @@ -28,9 +29,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineHitObjectBlueprint : SelectionBlueprint { - private const float thickness = 5; private const float shadow_radius = 5; - private const float circle_size = 34; + private const float circle_size = 38; public Action OnDragHandled; @@ -40,8 +40,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable indexInCurrentComboBindable; private Bindable comboIndexBindable; - private readonly Circle circle; - private readonly DragBar dragBar; + private readonly Container circle; + private readonly DragArea dragArea; private readonly List shadowComponents = new List(); private readonly Container mainComponents; private readonly OsuSpriteText comboIndexText; @@ -76,71 +76,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, - Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black), + Y = -1, + Font = OsuFont.Default.With(size: circle_size * 0.5f, weight: FontWeight.Regular), }, }); - circle = new Circle + circle = new ExtendableCircle { - Size = new Vector2(circle_size), + RelativeSizeAxes = Axes.X, + Size = new Vector2(1, circle_size), Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = shadow_radius, - Colour = Color4.Black - }, + Origin = Anchor.CentreLeft, }; - shadowComponents.Add(circle); + mainComponents.Add(circle); if (hitObject is IHasDuration) { - DragBar dragBarUnderlay; - Container extensionBar; - - mainComponents.AddRange(new Drawable[] + mainComponents.Add(dragArea = new DragArea(hitObject) { - extensionBar = new Container - { - Masking = true, - Size = new Vector2(1, thickness), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativePositionAxes = Axes.X, - RelativeSizeAxes = Axes.X, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = shadow_radius, - Colour = Color4.Black - }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - } - }, - circle, - // only used for drawing the shadow - dragBarUnderlay = new DragBar(null), - // cover up the shadow on the join - new Box - { - Height = thickness, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - }, - dragBar = new DragBar(hitObject) { OnDragHandled = e => OnDragHandled?.Invoke(e) }, + OnDragHandled = e => OnDragHandled?.Invoke(e) }); - - shadowComponents.Add(dragBarUnderlay); - shadowComponents.Add(extensionBar); - } - else - { - mainComponents.Add(circle); } updateShadows(); @@ -173,7 +129,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var comboColour = combo.GetComboColour(comboColours); if (HitObject is IHasDuration) - mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White); + mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f)); else mainComponents.Colour = comboColour; @@ -227,10 +183,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool ShouldBeConsideredForInput(Drawable child) => true; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => - base.ReceivePositionalInputAt(screenSpacePos) || - circle.ReceivePositionalInputAt(screenSpacePos) || - dragBar?.ReceivePositionalInputAt(screenSpacePos) == true; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.Contains(screenSpacePos); protected override void OnSelected() { @@ -256,7 +209,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { Type = EdgeEffectType.Shadow, Radius = shadow_radius, - Colour = State == SelectionState.Selected ? Color4.Orange : Color4.Black + Colour = Color4.Black.Opacity(0.4f) }; } } @@ -267,22 +220,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateShadows(); } - public override Quad SelectionQuad - { - get - { - // correctly include the circle in the selection quad region, as it is usually outside the blueprint itself. - var leftQuad = circle.ScreenSpaceDrawQuad; - var rightQuad = dragBar?.ScreenSpaceDrawQuad ?? ScreenSpaceDrawQuad; - - return new Quad(leftQuad.TopLeft, Vector2.ComponentMax(rightQuad.TopRight, leftQuad.TopRight), - leftQuad.BottomLeft, Vector2.ComponentMax(rightQuad.BottomRight, leftQuad.BottomRight)); - } - } + public override Quad SelectionQuad => circle.ScreenSpaceDrawQuad; public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft; - public class DragBar : Container + public class DragArea : Container { private readonly HitObject hitObject; @@ -293,13 +235,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public override bool HandlePositionalInput => hitObject != null; - public DragBar(HitObject hitObject) + public DragArea(HitObject hitObject) { this.hitObject = hitObject; - CornerRadius = 2; + CornerRadius = circle_size / 2; Masking = true; - Size = new Vector2(5, 1); + Size = new Vector2(circle_size, 1); Anchor = Anchor.CentreRight; Origin = Anchor.Centre; RelativePositionAxes = Axes.X; @@ -406,5 +348,71 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline changeHandler?.EndChange(); } } + + /// + /// A circle with externalised end caps so it can take up the full width of a relative width area. + /// + public class ExtendableCircle : Container + { + private readonly Circle rightCircle; + private readonly Circle leftCircle; + + public override Quad ScreenSpaceDrawQuad + { + get + { + var leftQuad = leftCircle.ScreenSpaceDrawQuad; + + if (Width == 0) + { + return leftQuad; + } + + var rightQuad = rightCircle.ScreenSpaceDrawQuad; + + return new Quad(leftQuad.TopLeft, rightQuad.TopRight, leftQuad.BottomLeft, rightQuad.BottomRight); + } + } + + public ExtendableCircle() + { + var effect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = shadow_radius, + Colour = Color4.Black.Opacity(0.4f) + }; + + InternalChildren = new Drawable[] + { + new Container + { + Height = circle_size, + RelativeSizeAxes = Axes.X, + Masking = true, + AlwaysPresent = true, + EdgeEffect = effect, + }, + leftCircle = new Circle + { + EdgeEffect = effect, + Origin = Anchor.TopCentre, + Size = new Vector2(circle_size) + }, + rightCircle = new Circle + { + EdgeEffect = effect, + Anchor = Anchor.TopRight, + Origin = Anchor.TopCentre, + Size = new Vector2(circle_size) + }, + new Box + { + Height = circle_size, + RelativeSizeAxes = Axes.X, + }, + }; + } + } } } From 109ee395bf397fc73b13e10f70bf8dedae67b20f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 18:47:07 +0900 Subject: [PATCH 60/65] Fix input and remove outdated hover logic --- .../Timeline/TimelineHitObjectBlueprint.cs | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 179abd0a26..89a9095d22 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -17,7 +17,6 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics; 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.Types; @@ -40,9 +39,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable indexInCurrentComboBindable; private Bindable comboIndexBindable; - private readonly Container circle; - private readonly DragArea dragArea; - private readonly List shadowComponents = new List(); + private readonly Drawable circle; + private readonly Container mainComponents; private readonly OsuSpriteText comboIndexText; @@ -93,7 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (hitObject is IHasDuration) { - mainComponents.Add(dragArea = new DragArea(hitObject) + mainComponents.Add(new DragArea(hitObject) { OnDragHandled = e => OnDragHandled?.Invoke(e) }); @@ -183,8 +181,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool ShouldBeConsideredForInput(Drawable child) => true; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.Contains(screenSpacePos); - protected override void OnSelected() { updateShadows(); @@ -192,27 +188,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateShadows() { - foreach (var s in shadowComponents) - { - if (State == SelectionState.Selected) - { - s.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = shadow_radius / 2, - Colour = Color4.Orange, - }; - } - else - { - s.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = shadow_radius, - Colour = Color4.Black.Opacity(0.4f) - }; - } - } } protected override void OnDeselected() @@ -220,6 +195,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateShadows(); } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + circle.ReceivePositionalInputAt(screenSpacePos); + public override Quad SelectionQuad => circle.ScreenSpaceDrawQuad; public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft; @@ -351,12 +329,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// /// A circle with externalised end caps so it can take up the full width of a relative width area. + /// TODO: figure how to do this with a single circle to avoid pixel-misaligned edges. /// public class ExtendableCircle : Container { private readonly Circle rightCircle; private readonly Circle leftCircle; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + return base.ReceivePositionalInputAt(screenSpacePos) + || leftCircle.ReceivePositionalInputAt(screenSpacePos) + || rightCircle.ReceivePositionalInputAt(screenSpacePos); + } + public override Quad ScreenSpaceDrawQuad { get From 495fdd8d65d99d7818726e29829fcd6ff8fed660 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 18:58:51 +0900 Subject: [PATCH 61/65] Update drag area display to match new design logic --- .../Timeline/TimelineHitObjectBlueprint.cs | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 89a9095d22..493f62921b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -202,7 +202,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft; - public class DragArea : Container + public class DragArea : Circle { private readonly HitObject hitObject; @@ -224,6 +224,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.Centre; RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Y; + Colour = OsuColour.Gray(0.2f); InternalChildren = new Drawable[] { @@ -234,6 +235,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + updateState(); + FinishTransforms(); + } + protected override bool OnHover(HoverEvent e) { updateState(); @@ -265,7 +274,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void updateState() { - Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White; + if (hasMouseDown) + { + this.ScaleTo(0.7f, 200, Easing.OutQuint); + } + else if (IsHovered) + { + this.ScaleTo(0.8f, 200, Easing.OutQuint); + } + else + { + this.ScaleTo(0.6f, 200, Easing.OutQuint); + } + + this.FadeTo(IsHovered || hasMouseDown ? 0.8f : 0.2f, 200, Easing.OutQuint); } [Resolved] @@ -369,12 +391,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Colour = Color4.Black.Opacity(0.4f) }; + const float fudge = 0.97f; + InternalChildren = new Drawable[] { new Container { - Height = circle_size, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = fudge, Masking = true, AlwaysPresent = true, EdgeEffect = effect, @@ -394,8 +420,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, new Box { - Height = circle_size, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = fudge, }, }; } From b2c17979defca0bd018244129c9b9e1d029fdc84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 19:24:20 +0900 Subject: [PATCH 62/65] Update colours of all overlay components in one swoop (based off combo colour) --- .../Timeline/TimelineHitObjectBlueprint.cs | 79 ++++++++++--------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 493f62921b..6463f1a200 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -31,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private const float shadow_radius = 5; private const float circle_size = 38; + private Container repeatsContainer; + public Action OnDragHandled; [UsedImplicitly] @@ -41,7 +43,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Drawable circle; - private readonly Container mainComponents; + private readonly Container colouredComponents; private readonly OsuSpriteText comboIndexText; [Resolved] @@ -59,39 +61,37 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + Height = circle_size; - AddRangeInternal(new Drawable[] + AddRangeInternal(new[] { - mainComponents = new Container + circle = new ExtendableCircle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + colouredComponents = new Container { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - comboIndexText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - Y = -1, - Font = OsuFont.Default.With(size: circle_size * 0.5f, weight: FontWeight.Regular), + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + comboIndexText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Y = -1, + Font = OsuFont.Default.With(size: circle_size * 0.5f, weight: FontWeight.Regular), + }, + } }, }); - circle = new ExtendableCircle - { - RelativeSizeAxes = Axes.X, - Size = new Vector2(1, circle_size), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }; - - mainComponents.Add(circle); - if (hitObject is IHasDuration) { - mainComponents.Add(new DragArea(hitObject) + colouredComponents.Add(new DragArea(hitObject) { OnDragHandled = e => OnDragHandled?.Invoke(e) }); @@ -127,15 +127,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var comboColour = combo.GetComboColour(comboColours); if (HitObject is IHasDuration) - mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f)); + circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f)); else - mainComponents.Colour = comboColour; + circle.Colour = comboColour; - var col = mainComponents.Colour.TopLeft.Linear; + var col = circle.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; + colouredComponents.Colour = OsuColour.Gray(brightness > 0.5f ? 0.2f : 0.9f); } protected override void Update() @@ -155,13 +155,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - private Container repeatsContainer; - private void updateRepeats(IHasRepeats repeats) { repeatsContainer?.Expire(); - mainComponents.Add(repeatsContainer = new Container + colouredComponents.Add(repeatsContainer = new Container { RelativeSizeAxes = Axes.Both, }); @@ -170,7 +168,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { repeatsContainer.Add(new Circle { - Size = new Vector2(circle_size / 2), + Size = new Vector2(circle_size / 3), + Alpha = 0.2f, Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, RelativePositionAxes = Axes.X, @@ -224,7 +223,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.Centre; RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Y; - Colour = OsuColour.Gray(0.2f); InternalChildren = new Drawable[] { @@ -351,7 +349,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// /// A circle with externalised end caps so it can take up the full width of a relative width area. - /// TODO: figure how to do this with a single circle to avoid pixel-misaligned edges. /// public class ExtendableCircle : Container { @@ -391,7 +388,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Colour = Color4.Black.Opacity(0.4f) }; - const float fudge = 0.97f; + // TODO: figure how to do this whole thing with a single circle to avoid pixel-misaligned edges. + // just working with what i can make work for the time being.. + const float fudge = 0.4f; InternalChildren = new Drawable[] { @@ -400,7 +399,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Height = fudge, + Padding = new MarginPadding { Vertical = fudge }, Masking = true, AlwaysPresent = true, EdgeEffect = effect, @@ -418,12 +417,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.TopCentre, Size = new Vector2(circle_size) }, - new Box + new Container { RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = fudge }, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Height = fudge, + Children = new Drawable[] + { + new Box { RelativeSizeAxes = Axes.Both, } + } }, }; } From e7b0042a608d09e1f6a01e53860a01595905177f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 19:25:03 +0900 Subject: [PATCH 63/65] Remove unnecessary hover / shadow logic --- .../Timeline/TimelineHitObjectBlueprint.cs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 6463f1a200..67be298567 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -28,7 +28,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineHitObjectBlueprint : SelectionBlueprint { - private const float shadow_radius = 5; private const float circle_size = 38; private Container repeatsContainer; @@ -96,8 +95,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline OnDragHandled = e => OnDragHandled?.Invoke(e) }); } - - updateShadows(); } protected override void LoadComplete() @@ -116,6 +113,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } + protected override void OnSelected() + { + // base logic hides selected blueprints when not selected, but timeline doesn't do that. + } + + protected override void OnDeselected() + { + // base logic hides selected blueprints when not selected, but timeline doesn't do that. + } + private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString(); private void updateComboColour() @@ -180,20 +187,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool ShouldBeConsideredForInput(Drawable child) => true; - protected override void OnSelected() - { - updateShadows(); - } - - private void updateShadows() - { - } - - protected override void OnDeselected() - { - updateShadows(); - } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos); @@ -384,7 +377,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var effect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, - Radius = shadow_radius, + Radius = 5, Colour = Color4.Black.Opacity(0.4f) }; From bcd41417b3cb7701985d8db8614ded82cc9c7034 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 19:36:29 +0900 Subject: [PATCH 64/65] 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 c78dfb6a55..b5315c3616 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 92e05cb4a6..45b3d5c161 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 11124730c9..105a6e59c2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From e601141be2a70e1a1f004c205b360212a9fe2605 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 13 Apr 2021 14:57:02 +0300 Subject: [PATCH 65/65] Simplify ExtendableCircle component --- .../Timeline/TimelineHitObjectBlueprint.cs | 83 ++++--------------- 1 file changed, 15 insertions(+), 68 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 67be298567..1c0d6e5ab6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -343,84 +343,31 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// /// A circle with externalised end caps so it can take up the full width of a relative width area. /// - public class ExtendableCircle : Container + public class ExtendableCircle : CompositeDrawable { - private readonly Circle rightCircle; - private readonly Circle leftCircle; + private readonly CircularContainer content; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - return base.ReceivePositionalInputAt(screenSpacePos) - || leftCircle.ReceivePositionalInputAt(screenSpacePos) - || rightCircle.ReceivePositionalInputAt(screenSpacePos); - } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => content.ReceivePositionalInputAt(screenSpacePos); - public override Quad ScreenSpaceDrawQuad - { - get - { - var leftQuad = leftCircle.ScreenSpaceDrawQuad; - - if (Width == 0) - { - return leftQuad; - } - - var rightQuad = rightCircle.ScreenSpaceDrawQuad; - - return new Quad(leftQuad.TopLeft, rightQuad.TopRight, leftQuad.BottomLeft, rightQuad.BottomRight); - } - } + public override Quad ScreenSpaceDrawQuad => content.ScreenSpaceDrawQuad; public ExtendableCircle() { - var effect = new EdgeEffectParameters + Padding = new MarginPadding { Horizontal = -circle_size / 2f }; + InternalChild = content = new CircularContainer { - Type = EdgeEffectType.Shadow, - Radius = 5, - Colour = Color4.Black.Opacity(0.4f) - }; - - // TODO: figure how to do this whole thing with a single circle to avoid pixel-misaligned edges. - // just working with what i can make work for the time being.. - const float fudge = 0.4f; - - InternalChildren = new Drawable[] - { - new Container + RelativeSizeAxes = Axes.Both, + Masking = true, + EdgeEffect = new EdgeEffectParameters { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Vertical = fudge }, - Masking = true, - AlwaysPresent = true, - EdgeEffect = effect, + Type = EdgeEffectType.Shadow, + Radius = 5, + Colour = Color4.Black.Opacity(0.4f) }, - leftCircle = new Circle + Child = new Box { - EdgeEffect = effect, - Origin = Anchor.TopCentre, - Size = new Vector2(circle_size) - }, - rightCircle = new Circle - { - EdgeEffect = effect, - Anchor = Anchor.TopRight, - Origin = Anchor.TopCentre, - Size = new Vector2(circle_size) - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Vertical = fudge }, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Children = new Drawable[] - { - new Box { RelativeSizeAxes = Axes.Both, } - } - }, + RelativeSizeAxes = Axes.Both + } }; } }