From 742698acab9ba2034ebae716bcd8cc1634d5a13e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 15:30:24 +0900 Subject: [PATCH 01/77] Add notelock implementation --- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 7 +++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 28 +++++++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index da1e666aba..3ca2714511 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) + if (result == HitResult.None || CheckHittable?.Invoke(this) == false) { Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss)); return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index a677cb6a72..82a81040e4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; @@ -16,6 +17,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. public override bool HandlePositionalInput => true; + /// + /// Whether this can be hit. + /// If not-null, this will not receive a judgement until this function returns true. + /// + public Func CheckHittable; + protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 6d1ea4bbfc..9eb2786951 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -64,7 +65,10 @@ namespace osu.Game.Rulesets.Osu.UI base.Add(h); - followPoints.AddFollowPoints((DrawableOsuHitObject)h); + DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + osuHitObject.CheckHittable = checkHittable; + + followPoints.AddFollowPoints(osuHitObject); } public override bool Remove(DrawableHitObject h) @@ -72,11 +76,31 @@ namespace osu.Game.Rulesets.Osu.UI bool result = base.Remove(h); if (result) - followPoints.RemoveFollowPoints((DrawableOsuHitObject)h); + { + DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + osuHitObject.CheckHittable = null; + + followPoints.RemoveFollowPoints(osuHitObject); + } return result; } + private bool checkHittable(DrawableOsuHitObject osuHitObject) + { + var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); + + // Ensure the last object is not alive anymore, in which case always allow the hit. + if (lastObject == null) + return true; + + // Ensure that either the last object has received a judgement or the hit time occurs after the last object's start time. + if (lastObject.Judged || Time.Current > lastObject.HitObject.StartTime) + return true; + + return false; + } + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!judgedObject.DisplayResult || !DisplayJudgements.Value) From 80a86102b65b5a2421ef75e1899ff609ae463cb8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:00:48 +0900 Subject: [PATCH 02/77] Add test --- .../TestSceneNoteLock.cs | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs new file mode 100644 index 0000000000..a7416671f6 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -0,0 +1,180 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneNoteLock : RateAdjustedBeatmapTestScene + { + private const double time_first_circle = 1500; + private const double time_second_circle = 1600; + + private static readonly Vector2 position_first_circle = Vector2.Zero; + private static readonly Vector2 position_second_circle = new Vector2(80); + + /// + /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Miss); + } + + /// + /// Tests clicking the second circle at the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAtFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Miss); + } + + /// + /// Tests clicking the second circle after the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Great); + } + + /// + /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(HitResult.Great, HitResult.Great); + } + + private void addJudgementAssert(HitResult firstCircle, HitResult secondCircle) + { + AddAssert($"first circle judgement is {firstCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).Type == firstCircle); + AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); + } + + private ScoreAccessibleReplayPlayer currentPlayer; + private List judgementResults; + private bool allJudgedFired; + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + p.ScoreProcessor.AllJudged += () => + { + if (currentPlayer == p) allJudgedFired = true; + }; + }; + + LoadScreen(currentPlayer = p); + allJudgedFired = false; + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for all judged", () => allJudgedFired); + } + + private class TestHitCircle : HitCircle + { + protected override HitWindows CreateHitWindows() => new TestHitWindows(); + } + + private class TestHitWindows : HitWindows + { + private static readonly DifficultyRange[] ranges = + { + new DifficultyRange(HitResult.Great, 500, 500, 500), + new DifficultyRange(HitResult.Miss, 1000, 1000, 1000), + }; + + public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; + + protected override DifficultyRange[] GetRanges() => ranges; + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, false, false) + { + } + } + } +} From 1d680b7a0073b783cee638e64c31d90c966f9deb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 Mar 2020 19:13:25 +0900 Subject: [PATCH 03/77] Better english Co-Authored-By: Dean Herbert --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 82a81040e4..3e66549ca0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Whether this can be hit. - /// If not-null, this will not receive a judgement until this function returns true. + /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// public Func CheckHittable; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9eb2786951..643253b1af 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.UI { var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); - // Ensure the last object is not alive anymore, in which case always allow the hit. + // If there is no previous object alive, allow the hit. if (lastObject == null) return true; From f285b43a74afd66c6c2ec1dcbe63e4f66f007314 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Mar 2020 17:44:32 +0900 Subject: [PATCH 04/77] Allow simultaneous hitobjects --- osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index a7416671f6..59d8727ae1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Miss); + addJudgementAssert(HitResult.Miss, HitResult.Great); } /// diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 643253b1af..bf91504b00 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -94,8 +94,9 @@ namespace osu.Game.Rulesets.Osu.UI if (lastObject == null) return true; - // Ensure that either the last object has received a judgement or the hit time occurs after the last object's start time. - if (lastObject.Judged || Time.Current > lastObject.HitObject.StartTime) + // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. + // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. + if (lastObject.Judged || Time.Current >= lastObject.HitObject.StartTime) return true; return false; From 12a48d2774dd0e4aa19cdd989b34c7022343ff1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Mar 2020 19:16:24 +0900 Subject: [PATCH 05/77] Cause all earlier hitobjects to get missed --- .../TestSceneNoteLock.cs | 13 ++++- .../Objects/Drawables/DrawableOsuHitObject.cs | 6 +++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index 59d8727ae1..e2b8364f3e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private const double time_first_circle = 1500; private const double time_second_circle = 1600; + private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss + private const double late_miss_window = 500; // time after +500 is considered a miss private static readonly Vector2 position_first_circle = Vector2.Zero; private static readonly Vector2 position_second_circle = new Vector2(80); @@ -40,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Miss); + addJudgementOffsetAssert(late_miss_window); } /// @@ -54,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Great); + addJudgementOffsetAssert(0); } /// @@ -68,6 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Great); + addJudgementOffsetAssert(100); } /// @@ -91,6 +97,11 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); } + private void addJudgementOffsetAssert(double offset) + { + AddAssert($"first circle judged at {offset}", () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).TimeOffset, offset, 100)); + } + private ScoreAccessibleReplayPlayer currentPlayer; private List judgementResults; private bool allJudgedFired; @@ -157,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Tests private static readonly DifficultyRange[] ranges = { new DifficultyRange(HitResult.Great, 500, 500, 500), - new DifficultyRange(HitResult.Miss, 1000, 1000, 1000), + new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), }; public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 3e66549ca0..13829dc2f7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -61,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + /// + /// Causes this to get missed, disregarding all conditions in implementations of . + /// + public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index bf91504b00..e36d32d01a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; @@ -104,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { + missAllEarlier(result); + if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -117,6 +120,55 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer.Add(explosion); } + /// + /// Misses all s occurring earlier than the start time of a judged . + /// + /// The of the judged . + private void missAllEarlier(JudgementResult result) + { + // Hitobjects that count as bonus should not cause other hitobjects to get missed. + // E.g. For the sequence slider-head -> circle -> slider-tick, hitting the tick before the circle should not cause the circle to be missed. + // E.g. For the sequence spinner -> circle -> spinner-bonus, hitting the bonus before the circle should not cause the circle to be missed. + if (result.Judgement.IsBonus) + return; + + // The minimum start time required for hitobjects so that they aren't missed. + double minimumTime = result.HitObject.StartTime; + + foreach (var obj in HitObjectContainer.AliveObjects) + { + if (obj.HitObject.StartTime >= minimumTime) + break; + + attemptMiss(obj); + + foreach (var n in obj.NestedHitObjects) + { + if (n.HitObject.StartTime >= minimumTime) + break; + + attemptMiss(n); + } + } + + static void attemptMiss(DrawableHitObject obj) + { + if (!(obj is DrawableOsuHitObject osuObject)) + throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); + + // Hitobjects that have already been judged cannot be missed. + if (osuObject.Judged) + return; + + // Hitobjects that count as bonus should not be missed. + // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. + if (osuObject.Result.Judgement.IsBonus) + return; + + osuObject.MissForcefully(); + } + } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From 7b24cc325f0ed61490766307ac0a681d5a9bb766 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 27 Mar 2020 20:57:57 +0300 Subject: [PATCH 06/77] Implement OverlayScrollContainer component --- .../TestSceneOverlayScrollContainer.cs | 90 ++++++++++ osu.Game/Overlays/OverlayScrollContainer.cs | 169 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs create mode 100644 osu.Game/Overlays/OverlayScrollContainer.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs new file mode 100644 index 0000000000..1fc85c3c04 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; +using NUnit.Framework; +using osu.Framework.Utils; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOverlayScrollContainer : OsuManualInputManagerTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(OverlayScrollContainer) + }; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + private OverlayScrollContainer scroll; + + private int invocationCount; + + [SetUp] + public void SetUp() => Schedule(() => + { + Add(scroll = new OverlayScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new Container + { + Height = 3000, + RelativeSizeAxes = Axes.X, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray + } + } + }); + + invocationCount = 0; + + scroll.Button.Action += () => invocationCount++; + }); + + [Test] + public void TestButtonVisibility() + { + AddAssert("button is hidden", () => scroll.Button.State.Value == Visibility.Hidden); + + AddStep("scroll to end", () => scroll.ScrollToEnd(false)); + AddAssert("button is visible", () => scroll.Button.State.Value == Visibility.Visible); + + AddStep("scroll to start", () => scroll.ScrollToStart(false)); + AddAssert("button is hidden", () => scroll.Button.State.Value == Visibility.Hidden); + } + + [Test] + public void TestButtonAction() + { + AddStep("scroll to end", () => scroll.ScrollToEnd(false)); + + AddStep("invoke action", () => scroll.Button.Action.Invoke()); + + AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); + } + + [Test] + public void TestMultipleClicks() + { + AddStep("scroll to end", () => scroll.ScrollToEnd(false)); + + AddAssert("invocation count is 0", () => invocationCount == 0); + + AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button)); + AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3); + + AddAssert("invocation count is 1", () => invocationCount == 1); + } + } +} diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs new file mode 100644 index 0000000000..1a875ded95 --- /dev/null +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -0,0 +1,169 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays +{ + /// + /// which provides . Mostly used in . + /// + public class OverlayScrollContainer : OsuScrollContainer + { + /// + /// Scroll position at which the will be shown. + /// + private const int button_scroll_position = 200; + + public ScrollToTopButton Button { get; } + + private float currentTarget; + + public OverlayScrollContainer() + { + AddInternal(Button = new ScrollToTopButton + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding(20), + Action = () => + { + ScrollToStart(); + currentTarget = Target; + Button.State.Value = Visibility.Hidden; + } + }); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight) + { + Button.State.Value = Visibility.Hidden; + return; + } + + if (Target == currentTarget) + return; + + currentTarget = Target; + Button.State.Value = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + } + + public class ScrollToTopButton : VisibilityContainer + { + private const int fade_duration = 500; + + public Action Action + { + get => button.Action; + set => button.Action = value; + } + + public override bool PropagatePositionalInputSubTree => true; + + protected override bool StartHidden => true; + + private readonly Button button; + + public ScrollToTopButton() + { + Size = new Vector2(50); + Child = button = new Button(); + } + + protected override bool OnMouseDown(MouseDownEvent e) => true; + + protected override void PopIn() => button.FadeIn(fade_duration, Easing.OutQuint); + + protected override void PopOut() => button.FadeOut(fade_duration, Easing.OutQuint); + + private class Button : OsuHoverContainer + { + public override bool PropagatePositionalInputSubTree => Alpha == 1; + + protected override IEnumerable EffectTargets => new[] { background }; + + private Color4 flashColour; + + private readonly Container content; + private readonly Box background; + + public Button() + { + RelativeSizeAxes = Axes.Both; + Add(content = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 3f, + Colour = Color4.Black.Opacity(0.25f), + }, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(15), + Icon = FontAwesome.Solid.ChevronUp + } + } + }); + + TooltipText = "Scroll to top"; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = colourProvider.Background6; + HoverColour = colourProvider.Background5; + flashColour = colourProvider.Light1; + } + + protected override bool OnClick(ClickEvent e) + { + background.FlashColour(flashColour, 800, Easing.OutQuint); + return base.OnClick(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + content.ScaleTo(0.75f, 2000, Easing.OutQuint); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + content.ScaleTo(1, 1000, Easing.OutElastic); + base.OnMouseUp(e); + } + } + } + } +} From b9277165f788361acb43b3c57da49ce605d44155 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 15:11:15 +0900 Subject: [PATCH 07/77] Refactor test to support custom hitobjects --- .../TestSceneNoteLock.cs | 126 +++++++++++++----- 1 file changed, 94 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index e2b8364f3e..af82a05c4f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -23,8 +24,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneNoteLock : RateAdjustedBeatmapTestScene { - private const double time_first_circle = 1500; - private const double time_second_circle = 1600; private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss @@ -37,13 +36,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleBeforeFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Miss); - addJudgementOffsetAssert(late_miss_window); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); } /// @@ -52,13 +69,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleAtFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Great); - addJudgementOffsetAssert(0); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], 0); } /// @@ -67,13 +102,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleAfterFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Great); - addJudgementOffsetAssert(100); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], 100); } /// @@ -82,49 +135,58 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } }); - addJudgementAssert(HitResult.Great, HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } - private void addJudgementAssert(HitResult firstCircle, HitResult secondCircle) + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { - AddAssert($"first circle judgement is {firstCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).Type == firstCircle); - AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); } - private void addJudgementOffsetAssert(double offset) + private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) { - AddAssert($"first circle judged at {offset}", () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).TimeOffset, offset, 100)); + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", + () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); } private ScoreAccessibleReplayPlayer currentPlayer; private List judgementResults; private bool allJudgedFired; - private void performTest(List frames) + private void performTest(List hitObjects, List frames) { AddStep("load player", () => { Beatmap.Value = CreateWorkingBeatmap(new Beatmap { - HitObjects = - { - new TestHitCircle - { - StartTime = time_first_circle, - Position = position_first_circle - }, - new TestHitCircle - { - StartTime = time_second_circle, - Position = position_second_circle - } - }, + HitObjects = hitObjects, BeatmapInfo = { BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, From 0d202929921e26afb043ea637bb7c9a722b0f7d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:14:56 +0900 Subject: [PATCH 08/77] Fix ticks/spinners contributing to notelock --- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/DrawableSliderHead.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Objects/SliderHeadCircle.cs | 9 ++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 53 +++++++++++-------- 5 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5c7f4a42b3..b017eacf70 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case SliderTailCircle tail: return new DrawableSliderTail(slider, tail); - case HitCircle head: + case SliderHeadCircle head: return new DrawableSliderHead(slider, head) { OnShake = Shake }; case SliderTick tick: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c5609b01e0..563282e18f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Slider slider; - public DrawableSliderHead(Slider slider, HitCircle h) + public DrawableSliderHead(Slider slider, SliderHeadCircle h) : base(h) { this.slider = slider; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index db1f46d8e2..e5d6c20738 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Head: - AddNested(HeadCircle = new SliderCircle + AddNested(HeadCircle = new SliderHeadCircle { StartTime = e.Time, Position = Position, diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs new file mode 100644 index 0000000000..f6d46aeef5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -0,0 +1,9 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SliderHeadCircle : HitCircle + { + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index e36d32d01a..97e002edd0 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -126,10 +127,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The of the judged . private void missAllEarlier(JudgementResult result) { - // Hitobjects that count as bonus should not cause other hitobjects to get missed. - // E.g. For the sequence slider-head -> circle -> slider-tick, hitting the tick before the circle should not cause the circle to be missed. - // E.g. For the sequence spinner -> circle -> spinner-bonus, hitting the bonus before the circle should not cause the circle to be missed. - if (result.Judgement.IsBonus) + if (!contributesToNoteLock(result.HitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. @@ -140,35 +138,44 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - attemptMiss(obj); + performMiss(obj); foreach (var n in obj.NestedHitObjects) { if (n.HitObject.StartTime >= minimumTime) break; - attemptMiss(n); + performMiss(n); } } - - static void attemptMiss(DrawableHitObject obj) - { - if (!(obj is DrawableOsuHitObject osuObject)) - throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); - - // Hitobjects that have already been judged cannot be missed. - if (osuObject.Judged) - return; - - // Hitobjects that count as bonus should not be missed. - // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. - if (osuObject.Result.Judgement.IsBonus) - return; - - osuObject.MissForcefully(); - } } + private void performMiss(DrawableHitObject obj) + { + if (!(obj is DrawableOsuHitObject osuObject)) + throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); + + // Hitobjects that have already been judged cannot be missed. + if (osuObject.Judged) + return; + + // Hitobjects that count as bonus should not be missed. + // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. + if (!contributesToNoteLock(obj.HitObject)) + return; + + osuObject.MissForcefully(); + } + + /// + /// Whether a hitobject contributes to notelock. + /// Only hit circles and slider start circles contribute to notelock. + /// + /// The hitobject to test. + /// Whether contributes to notelock. + private bool contributesToNoteLock(HitObject hitObject) + => hitObject is HitCircle && !(hitObject is SliderTailCircle); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From e074c3e5e99f69349471cf21e8a72323f390417b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:15:07 +0900 Subject: [PATCH 09/77] Add additional tests --- .../TestSceneNoteLock.cs | 182 ++++++++++++++++-- 1 file changed, 166 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index af82a05c4f..a33fb54ff6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -11,6 +12,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Replays; @@ -27,9 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss - private static readonly Vector2 position_first_circle = Vector2.Zero; - private static readonly Vector2 position_second_circle = new Vector2(80); - /// /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. /// @@ -38,24 +38,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -71,24 +73,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -104,24 +108,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -137,25 +143,27 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } + new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); addJudgementAssert(hitObjects[0], HitResult.Great); @@ -164,12 +172,133 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } + [Test] + public void TestMissSliderHeadAndHitAllSliderTicks() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + } + + [Test] + public void TestHitSliderTicksBeforeCircle() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + } + + [Test] + public void TestHitCircleBeforeSpinner() + { + const double time_spinner = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + + var hitObjects = new List + { + new TestSpinner + { + StartTime = time_spinner, + Position = new Vector2(256, 192), + EndTime = time_spinner + 1000, + }, + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_spinner, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + } + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); } + private void addJudgementAssert(string name, Func hitObject, HitResult result) + { + AddAssert($"{name} judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); + } + private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", @@ -225,6 +354,27 @@ namespace osu.Game.Rulesets.Osu.Tests protected override HitWindows CreateHitWindows() => new TestHitWindows(); } + private class TestSlider : Slider + { + public TestSlider() + { + DefaultsApplied += () => + { + HeadCircle.HitWindows = new TestHitWindows(); + TailCircle.HitWindows = new TestHitWindows(); + }; + } + } + + private class TestSpinner : Spinner + { + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + SpinsRequired = 1; + } + } + private class TestHitWindows : HitWindows { private static readonly DifficultyRange[] ranges = From 744f6c3ca7be99511c5732c0fa4c8688a3acbd5e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:33:46 +0900 Subject: [PATCH 10/77] Rename method + adjust comments --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 97e002edd0..994b3d9718 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The of the judged . private void missAllEarlier(JudgementResult result) { - if (!contributesToNoteLock(result.HitObject)) + if (!causesNoteLockMisses(result.HitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. @@ -159,21 +159,18 @@ namespace osu.Game.Rulesets.Osu.UI if (osuObject.Judged) return; - // Hitobjects that count as bonus should not be missed. - // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. - if (!contributesToNoteLock(obj.HitObject)) + if (!causesNoteLockMisses(obj.HitObject)) return; osuObject.MissForcefully(); } /// - /// Whether a hitobject contributes to notelock. - /// Only hit circles and slider start circles contribute to notelock. + /// Whether a can be missed and causes other hitobjects to be missed during notelock. /// - /// The hitobject to test. - /// Whether contributes to notelock. - private bool contributesToNoteLock(HitObject hitObject) + /// The to test. + /// Whether contributes to notelock misses. + private bool causesNoteLockMisses(HitObject hitObject) => hitObject is HitCircle && !(hitObject is SliderTailCircle); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); From 796976db3c046f967e0403c9d15e4d305cbe3435 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:00:53 +0900 Subject: [PATCH 11/77] Completely ignore spinners from note lock --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 994b3d9718..db8a47e4a2 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -90,7 +90,14 @@ namespace osu.Game.Rulesets.Osu.UI private bool checkHittable(DrawableOsuHitObject osuHitObject) { - var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); + DrawableHitObject lastObject = osuHitObject; + + // Get the last hitobject that contributes to note lock + while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + { + if (contributesToNoteLock(lastObject.HitObject)) + break; + } // If there is no previous object alive, allow the hit. if (lastObject == null) @@ -166,10 +173,19 @@ namespace osu.Game.Rulesets.Osu.UI } /// - /// Whether a can be missed and causes other hitobjects to be missed during notelock. + /// Whether a is contributes to note lock. + /// Future contributing s will not be hittable until the start time of the last contributing is reached. /// /// The to test. - /// Whether contributes to notelock misses. + /// Whether causes note lock. + private bool contributesToNoteLock(HitObject hitObject) + => hitObject is HitCircle || hitObject is Slider; + + /// + /// Whether a can be missed and causes other s to be missed when hit out-of-order during note lock. + /// + /// The to test. + /// Whether contributes to note lock misses. private bool causesNoteLockMisses(HitObject hitObject) => hitObject is HitCircle && !(hitObject is SliderTailCircle); From 1ff60b73d70deb25d9133f42b17b813711759bbc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:01:29 +0900 Subject: [PATCH 12/77] Refactor tests a bit --- .../TestSceneNoteLock.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index a33fb54ff6..2c69540951 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests private const double late_miss_window = 500; // time after +500 is considered a miss /// - /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleBeforeFirstCircleTime() @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle at the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleAtFirstCircleTime() @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle after the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleAfterFirstCircleTime() @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS been judged. + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged. /// [Test] public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() @@ -172,6 +172,9 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } + /// + /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks. + /// [Test] public void TestMissSliderHeadAndHitAllSliderTicks() { @@ -211,6 +214,9 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); } + /// + /// Tests clicking hitting future slider ticks before a circle. + /// [Test] public void TestHitSliderTicksBeforeCircle() { @@ -251,11 +257,14 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); } + /// + /// Tests clicking a future circle before a spinner. + /// [Test] public void TestHitCircleBeforeSpinner() { const double time_spinner = 1500; - const double time_circle = 1510; + const double time_circle = 1800; Vector2 positionCircle = Vector2.Zero; var hitObjects = new List @@ -275,7 +284,7 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_spinner, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, From 35647d59a6f46e9f9284995edcd43af377954158 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 19:09:05 +0900 Subject: [PATCH 13/77] Add failing test --- .../TestSceneOverlayScrollContainer.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 1fc85c3c04..684436459f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -74,6 +74,20 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); } + [Test] + public void TestClick() + { + AddStep("scroll to end", () => scroll.ScrollToEnd(false)); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(scroll.Button); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); + } + [Test] public void TestMultipleClicks() { From 179bd1ce7ebe2384fc819bdac20865f5176fd7d7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 30 Mar 2020 13:38:04 +0300 Subject: [PATCH 14/77] Fix failing test --- osu.Game/Overlays/OverlayScrollContainer.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 1a875ded95..a9524b9d32 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -83,7 +84,10 @@ namespace osu.Game.Overlays public ScrollToTopButton() { Size = new Vector2(50); - Child = button = new Button(); + Child = button = new Button + { + AreaState = { BindTarget = State } + }; } protected override bool OnMouseDown(MouseDownEvent e) => true; @@ -94,7 +98,9 @@ namespace osu.Game.Overlays private class Button : OsuHoverContainer { - public override bool PropagatePositionalInputSubTree => Alpha == 1; + public readonly Bindable AreaState = new Bindable(); + + public override bool HandlePositionalInput => AreaState.Value == Visibility.Visible; protected override IEnumerable EffectTargets => new[] { background }; From e26fbd5ed87681d8577fc5d9dced39cfd199b9cf Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 31 Mar 2020 13:45:59 +0300 Subject: [PATCH 15/77] Remove overcomplicated stuff --- .../TestSceneOverlayScrollContainer.cs | 6 +- osu.Game/Overlays/OverlayScrollContainer.cs | 159 ++++++++---------- 2 files changed, 75 insertions(+), 90 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 684436459f..0eccc907a1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -55,13 +55,13 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestButtonVisibility() { - AddAssert("button is hidden", () => scroll.Button.State.Value == Visibility.Hidden); + AddAssert("button is hidden", () => scroll.Button.Current.Value == Visibility.Hidden); AddStep("scroll to end", () => scroll.ScrollToEnd(false)); - AddAssert("button is visible", () => scroll.Button.State.Value == Visibility.Visible); + AddAssert("button is visible", () => scroll.Button.Current.Value == Visibility.Visible); AddStep("scroll to start", () => scroll.ScrollToStart(false)); - AddAssert("button is hidden", () => scroll.Button.State.Value == Visibility.Hidden); + AddAssert("button is hidden", () => scroll.Button.Current.Value == Visibility.Hidden); } [Test] diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index a9524b9d32..f96d9e3a31 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osuTK; @@ -43,7 +43,7 @@ namespace osu.Game.Overlays { ScrollToStart(); currentTarget = Target; - Button.State.Value = Visibility.Hidden; + Button.Current.Value = Visibility.Hidden; } }); } @@ -54,7 +54,7 @@ namespace osu.Game.Overlays if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight) { - Button.State.Value = Visibility.Hidden; + Button.Current.Value = Visibility.Hidden; return; } @@ -62,113 +62,98 @@ namespace osu.Game.Overlays return; currentTarget = Target; - Button.State.Value = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + Button.Current.Value = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } - public class ScrollToTopButton : VisibilityContainer + public class ScrollToTopButton : OsuHoverContainer, IHasCurrentValue { private const int fade_duration = 500; - public Action Action + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current { - get => button.Action; - set => button.Action = value; + get => current.Current; + set => current.Current = value; } - public override bool PropagatePositionalInputSubTree => true; + protected override IEnumerable EffectTargets => new[] { background }; - protected override bool StartHidden => true; + private Color4 flashColour; - private readonly Button button; + private readonly Container content; + private readonly Box background; public ScrollToTopButton() { Size = new Vector2(50); - Child = button = new Button + Alpha = 0; + Add(content = new CircularContainer { - AreaState = { BindTarget = State } - }; + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 3f, + Colour = Color4.Black.Opacity(0.25f), + }, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(15), + Icon = FontAwesome.Solid.ChevronUp + } + } + }); + + TooltipText = "Scroll to top"; } - protected override bool OnMouseDown(MouseDownEvent e) => true; - - protected override void PopIn() => button.FadeIn(fade_duration, Easing.OutQuint); - - protected override void PopOut() => button.FadeOut(fade_duration, Easing.OutQuint); - - private class Button : OsuHoverContainer + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - public readonly Bindable AreaState = new Bindable(); + IdleColour = colourProvider.Background6; + HoverColour = colourProvider.Background5; + flashColour = colourProvider.Light1; + } - public override bool HandlePositionalInput => AreaState.Value == Visibility.Visible; - - protected override IEnumerable EffectTargets => new[] { background }; - - private Color4 flashColour; - - private readonly Container content; - private readonly Box background; - - public Button() + protected override void LoadComplete() + { + base.LoadComplete(); + Current.BindValueChanged(visibility => { - RelativeSizeAxes = Axes.Both; - Add(content = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 1f), - Radius = 3f, - Colour = Color4.Black.Opacity(0.25f), - }, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(15), - Icon = FontAwesome.Solid.ChevronUp - } - } - }); + Enabled.Value = visibility.NewValue == Visibility.Visible; + this.FadeTo(visibility.NewValue == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint); + }, true); + } - TooltipText = "Scroll to top"; - } + protected override bool OnClick(ClickEvent e) + { + background.FlashColour(flashColour, 800, Easing.OutQuint); + return base.OnClick(e); + } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - IdleColour = colourProvider.Background6; - HoverColour = colourProvider.Background5; - flashColour = colourProvider.Light1; - } + protected override bool OnMouseDown(MouseDownEvent e) + { + content.ScaleTo(0.75f, 2000, Easing.OutQuint); + return true; + } - protected override bool OnClick(ClickEvent e) - { - background.FlashColour(flashColour, 800, Easing.OutQuint); - return base.OnClick(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - content.ScaleTo(0.75f, 2000, Easing.OutQuint); - return true; - } - - protected override void OnMouseUp(MouseUpEvent e) - { - content.ScaleTo(1, 1000, Easing.OutElastic); - base.OnMouseUp(e); - } + protected override void OnMouseUp(MouseUpEvent e) + { + content.ScaleTo(1, 1000, Easing.OutElastic); + base.OnMouseUp(e); } } } From 134feefa141b27b81ca9674a8332257216ee4755 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Apr 2020 13:10:09 +0300 Subject: [PATCH 16/77] Remove bindable --- .../TestSceneOverlayScrollContainer.cs | 6 ++-- osu.Game/Overlays/OverlayScrollContainer.cs | 36 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 0eccc907a1..4205d65100 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -55,13 +55,13 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestButtonVisibility() { - AddAssert("button is hidden", () => scroll.Button.Current.Value == Visibility.Hidden); + AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); AddStep("scroll to end", () => scroll.ScrollToEnd(false)); - AddAssert("button is visible", () => scroll.Button.Current.Value == Visibility.Visible); + AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); AddStep("scroll to start", () => scroll.ScrollToStart(false)); - AddAssert("button is hidden", () => scroll.Button.Current.Value == Visibility.Hidden); + AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); } [Test] diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index f96d9e3a31..a6c687f28f 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -3,14 +3,12 @@ using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osuTK; @@ -43,7 +41,7 @@ namespace osu.Game.Overlays { ScrollToStart(); currentTarget = Target; - Button.Current.Value = Visibility.Hidden; + Button.State = Visibility.Hidden; } }); } @@ -54,7 +52,7 @@ namespace osu.Game.Overlays if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight) { - Button.Current.Value = Visibility.Hidden; + Button.State = Visibility.Hidden; return; } @@ -62,19 +60,27 @@ namespace osu.Game.Overlays return; currentTarget = Target; - Button.Current.Value = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + Button.State = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } - public class ScrollToTopButton : OsuHoverContainer, IHasCurrentValue + public class ScrollToTopButton : OsuHoverContainer { private const int fade_duration = 500; - private readonly BindableWithCurrent current = new BindableWithCurrent(); + private Visibility state; - public Bindable Current + public Visibility State { - get => current.Current; - set => current.Current = value; + get => state; + set + { + if (value == state) + return; + + state = value; + Enabled.Value = state == Visibility.Visible; + this.FadeTo(state == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint); + } } protected override IEnumerable EffectTargets => new[] { background }; @@ -128,16 +134,6 @@ namespace osu.Game.Overlays flashColour = colourProvider.Light1; } - protected override void LoadComplete() - { - base.LoadComplete(); - Current.BindValueChanged(visibility => - { - Enabled.Value = visibility.NewValue == Visibility.Visible; - this.FadeTo(visibility.NewValue == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint); - }, true); - } - protected override bool OnClick(ClickEvent e) { background.FlashColour(flashColour, 800, Easing.OutQuint); From e58bf8a0d08154d87043f1a90170e8f46f6eea8c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 20:38:38 +0900 Subject: [PATCH 17/77] Add DiffPlex package --- osu.Game/osu.Game.csproj | 1 + osu.iOS.props | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3e2c2b1599..3dd84caea9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,6 +18,7 @@ + diff --git a/osu.iOS.props b/osu.iOS.props index 7903d964ce..7e6f6b5246 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -75,6 +75,7 @@ + From 86243d463f423367c2f140be93d00986c044894b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 20:48:59 +0900 Subject: [PATCH 18/77] Add legacy beatmap diffing --- .../Editor/LegacyEditorBeatmapDifferTest.cs | 342 ++++++++++++++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 40 +- .../Screens/Edit/LegacyEditorBeatmapDiffer.cs | 110 ++++++ 3 files changed, 488 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs create mode 100644 osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs new file mode 100644 index 0000000000..d70a112b7f --- /dev/null +++ b/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs @@ -0,0 +1,342 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Text; +using NUnit.Framework; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osuTK; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +namespace osu.Game.Tests.Editor +{ + [TestFixture] + public class LegacyEditorBeatmapDifferTest + { + private LegacyEditorBeatmapDiffer differ; + private EditorBeatmap current; + + [SetUp] + public void Setup() + { + differ = new LegacyEditorBeatmapDiffer(current = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + } + })); + } + + [Test] + public void TestAddHitObject() + { + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 1000 } + } + }; + + runTest(patch); + } + + [Test] + public void TestInsertHitObject() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + new HitCircle { StartTime = 2000 }, + (OsuHitObject)current.HitObjects[1], + } + }; + + runTest(patch); + } + + [Test] + public void TestDeleteHitObject() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + (OsuHitObject)current.HitObjects[2], + } + }; + + runTest(patch); + } + + [Test] + public void TestChangeStartTime() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 500 }, + (OsuHitObject)current.HitObjects[1], + (OsuHitObject)current.HitObjects[2], + } + }; + + runTest(patch); + } + + [Test] + public void TestChangeSample() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, + (OsuHitObject)current.HitObjects[2], + } + }; + + runTest(patch); + } + + [Test] + public void TestChangeSliderPath() + { + current.AddRange(new OsuHitObject[] + { + new HitCircle { StartTime = 1000 }, + new Slider + { + StartTime = 2000, + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero), + new PathControlPoint(Vector2.One), + new PathControlPoint(new Vector2(2), PathType.Bezier), + new PathControlPoint(new Vector2(3)), + }, 50) + }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + new Slider + { + StartTime = 2000, + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.Bezier), + new PathControlPoint(new Vector2(4)), + new PathControlPoint(new Vector2(5)), + }, 100) + }, + (OsuHitObject)current.HitObjects[2], + } + }; + + runTest(patch); + } + + [Test] + public void TestAddMultipleHitObjects() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 500 }, + (OsuHitObject)current.HitObjects[0], + new HitCircle { StartTime = 1500 }, + (OsuHitObject)current.HitObjects[1], + new HitCircle { StartTime = 2250 }, + new HitCircle { StartTime = 2500 }, + (OsuHitObject)current.HitObjects[2], + new HitCircle { StartTime = 3500 }, + } + }; + + runTest(patch); + } + + [Test] + public void TestDeleteMultipleHitObjects() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1500 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 2250 }, + new HitCircle { StartTime = 2500 }, + new HitCircle { StartTime = 3000 }, + new HitCircle { StartTime = 3500 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[1], + (OsuHitObject)current.HitObjects[3], + (OsuHitObject)current.HitObjects[6], + } + }; + + runTest(patch); + } + + [Test] + public void TestChangeSamplesOfMultipleHitObjects() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1500 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 2250 }, + new HitCircle { StartTime = 2500 }, + new HitCircle { StartTime = 3000 }, + new HitCircle { StartTime = 3500 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, + (OsuHitObject)current.HitObjects[2], + (OsuHitObject)current.HitObjects[3], + new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } }, + (OsuHitObject)current.HitObjects[5], + new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } }, + (OsuHitObject)current.HitObjects[7], + } + }; + + runTest(patch); + } + + [Test] + public void TestAddAndDeleteHitObjects() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1500 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 2250 }, + new HitCircle { StartTime = 2500 }, + new HitCircle { StartTime = 3000 }, + new HitCircle { StartTime = 3500 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 750 }, + (OsuHitObject)current.HitObjects[1], + (OsuHitObject)current.HitObjects[4], + (OsuHitObject)current.HitObjects[5], + new HitCircle { StartTime = 2650 }, + new HitCircle { StartTime = 2750 }, + new HitCircle { StartTime = 4000 }, + } + }; + + runTest(patch); + } + + private void runTest(IBeatmap patch) + { + // Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder. + // This causes issues because the decoder adds various default properties (e.g. new combo on first object, default samples). + // To resolve "patch" into a sane state it is encoded and then re-decoded. + patch = decode(encode(patch)); + + // Apply the patch. + differ.Patch(encode(current), encode(patch)); + + // Convert beatmaps to strings for assertion purposes. + string currentStr = Encoding.ASCII.GetString(encode(current).ToArray()); + string patchStr = Encoding.ASCII.GetString(encode(patch).ToArray()); + + Assert.That(currentStr, Is.EqualTo(patchStr)); + } + + private MemoryStream encode(IBeatmap beatmap) + { + var encoded = new MemoryStream(); + + using (var sw = new StreamWriter(encoded, leaveOpen: true)) + new LegacyBeatmapEncoder(beatmap).Encode(sw); + + return encoded; + } + + private IBeatmap decode(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + + using (var reader = new LineBufferedReader(stream, true)) + return Decoder.GetDecoder(reader).Decode(reader); + } + } +} diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 7f04a7a58d..22e0061b61 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -107,6 +107,16 @@ namespace osu.Game.Screens.Edit private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; + /// + /// Adds a collection of s to this . + /// + /// The s to add. + public void AddRange(IEnumerable hitObjects) + { + foreach (var h in hitObjects) + Add(h); + } + /// /// Adds a to this . /// @@ -128,12 +138,34 @@ namespace osu.Game.Screens.Edit /// Removes a from this . /// /// The to add. - public void Remove(HitObject hitObject) + /// True if the has been removed, false otherwise. + public bool Remove(HitObject hitObject) { - if (!mutableHitObjects.Contains(hitObject)) - return; + int index = FindIndex(hitObject); - mutableHitObjects.Remove(hitObject); + if (index == -1) + return false; + + RemoveAt(index); + return true; + } + + /// + /// Finds the index of a in this . + /// + /// The to search for. + /// The index of . + public int FindIndex(HitObject hitObject) => mutableHitObjects.IndexOf(hitObject); + + /// + /// Removes a at an index in this . + /// + /// The index of the to remove. + public void RemoveAt(int index) + { + var hitObject = (HitObject)mutableHitObjects[index]; + + mutableHitObjects.RemoveAt(index); var bindable = startTimeBindables[hitObject]; bindable.UnbindAll(); diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs new file mode 100644 index 0000000000..8d2f577a1d --- /dev/null +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using DiffPlex; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; + +namespace osu.Game.Screens.Edit +{ + public class LegacyEditorBeatmapDiffer + { + private readonly EditorBeatmap editorBeatmap; + + public LegacyEditorBeatmapDiffer(EditorBeatmap editorBeatmap) + { + this.editorBeatmap = editorBeatmap; + } + + public void Patch(Stream currentState, Stream newState) + { + // Diff the beatmaps + var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false); + + // Find the index of [HitObject] sections. Lines changed prior to this index are ignored. + int oldHitObjectsIndex = Array.IndexOf(result.PiecesOld, "[HitObjects]"); + int newHitObjectsIndex = Array.IndexOf(result.PiecesNew, "[HitObjects]"); + + var toRemove = new List(); + var toAdd = new List(); + + foreach (var block in result.DiffBlocks) + { + // Removed hitobject + for (int i = 0; i < block.DeleteCountA; i++) + { + int hoIndex = block.DeleteStartA + i - oldHitObjectsIndex - 1; + + if (hoIndex < 0) + continue; + + toRemove.Add(hoIndex); + } + + // Added hitobject + for (int i = 0; i < block.InsertCountB; i++) + { + int hoIndex = block.InsertStartB + i - newHitObjectsIndex - 1; + + if (hoIndex < 0) + continue; + + toAdd.Add(hoIndex); + } + } + + // Make the removal indices are sorted so that iteration order doesn't get messed up post-removal. + toRemove.Sort(); + + // Apply the changes. + for (int i = toRemove.Count - 1; i >= 0; i--) + editorBeatmap.RemoveAt(toRemove[i]); + + if (toAdd.Count > 0) + { + IBeatmap newBeatmap = readBeatmap(newState); + foreach (var i in toAdd) + editorBeatmap.Add(newBeatmap.HitObjects[i]); + } + } + + private string readString(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + + using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8, true, 1024, true)) + return sr.ReadToEnd(); + } + + private IBeatmap readBeatmap(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + + using (var reader = new LineBufferedReader(stream, true)) + return new PassThroughWorkingBeatmap(Decoder.GetDecoder(reader).Decode(reader)).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset); + } + + private class PassThroughWorkingBeatmap : WorkingBeatmap + { + private readonly IBeatmap beatmap; + + public PassThroughWorkingBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + + protected override Texture GetBackground() => throw new NotImplementedException(); + + protected override Track GetTrack() => throw new NotImplementedException(); + } + } +} From 14eca3655b752222b609b19f5fe268d1f467e6e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 21:22:07 +0900 Subject: [PATCH 19/77] Add change state handling to the editor --- osu.Game/Screens/Edit/Editor.cs | 30 ++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 108 ++++++++++++++++++ osu.Game/Screens/Edit/IEditorChangeHandler.cs | 33 ++++++ 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/EditorChangeHandler.cs create mode 100644 osu.Game/Screens/Edit/IEditorChangeHandler.cs diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index f1cbed57f1..14a227eb07 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; + private EditorChangeHandler changeHandler; private DependencyContainer dependencies; @@ -100,9 +101,11 @@ namespace osu.Game.Screens.Edit } AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); - dependencies.CacheAs(editorBeatmap); + changeHandler = new EditorChangeHandler(editorBeatmap); + dependencies.CacheAs(changeHandler); + EditorMenuBar menuBar; var fileMenuItems = new List @@ -147,6 +150,14 @@ namespace osu.Game.Screens.Edit new MenuItem("File") { Items = fileMenuItems + }, + new MenuItem("Edit") + { + Items = new[] + { + new EditorMenuItem("Undo", MenuItemType.Standard, undo), + new EditorMenuItem("Redo", MenuItemType.Standard, redo) + } } } } @@ -233,6 +244,19 @@ namespace osu.Game.Screens.Edit return true; } + break; + + case Key.Z: + if (e.ControlPressed) + { + if (e.ShiftPressed) + redo(); + else + undo(); + + return true; + } + break; } @@ -297,6 +321,10 @@ namespace osu.Game.Screens.Edit return base.OnExiting(next); } + private void undo() => changeHandler.RestoreState(-1); + + private void redo() => changeHandler.RestoreState(1); + private void resetTrack(bool seekToStart = false) { Beatmap.Value.Track?.Stop(); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs new file mode 100644 index 0000000000..7e372926ba --- /dev/null +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + /// + /// Tracks changes to the . + /// + public class EditorChangeHandler : IEditorChangeHandler + { + private readonly LegacyEditorBeatmapDiffer differ; + private readonly List savedStates = new List(); + private int currentState = -1; + + private readonly EditorBeatmap editorBeatmap; + private int bulkChangesStarted; + private bool isRestoring; + + /// + /// Creates a new . + /// + /// The to track the s of. + public EditorChangeHandler(EditorBeatmap editorBeatmap) + { + this.editorBeatmap = editorBeatmap; + + editorBeatmap.HitObjectAdded += hitObjectAdded; + editorBeatmap.HitObjectRemoved += hitObjectRemoved; + editorBeatmap.HitObjectUpdated += hitObjectUpdated; + + differ = new LegacyEditorBeatmapDiffer(editorBeatmap); + + // Initial state. + SaveState(); + } + + private void hitObjectAdded(HitObject obj) => SaveState(); + + private void hitObjectRemoved(HitObject obj) => SaveState(); + + private void hitObjectUpdated(HitObject obj) => SaveState(); + + public void BeginChange() => bulkChangesStarted++; + + public void EndChange() + { + if (bulkChangesStarted == 0) + throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}."); + + if (--bulkChangesStarted == 0) + SaveState(); + } + + /// + /// Saves the current state. + /// + public void SaveState() + { + if (bulkChangesStarted > 0) + return; + + if (isRestoring) + return; + + var stream = new MemoryStream(); + + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + + if (currentState < savedStates.Count - 1) + savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); + + savedStates.Add(stream); + currentState = savedStates.Count - 1; + } + + /// + /// Restores an older or newer state. + /// + /// The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used. + public void RestoreState(int direction) + { + if (bulkChangesStarted > 0) + return; + + if (savedStates.Count == 0) + return; + + int newState = Math.Clamp(currentState + direction, 0, savedStates.Count - 1); + if (currentState == newState) + return; + + isRestoring = true; + + differ.Patch(savedStates[currentState], savedStates[newState]); + currentState = newState; + + isRestoring = false; + } + } +} diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs new file mode 100644 index 0000000000..c1328252d4 --- /dev/null +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + /// + /// Interface for a component that manages changes in the . + /// + public interface IEditorChangeHandler + { + /// + /// Begins a bulk state change event. should be invoked soon after. + /// + /// + /// This should be invoked when multiple changes to the should be bundled together into one state change event. + /// When nested invocations are involved, a state change will not occur until an equal number of invocations of are received. + /// + /// + /// When a group of s are deleted, a single undo and redo state change should update the state of all . + /// + void BeginChange(); + + /// + /// Ends a bulk state change event. + /// + /// + /// This should be invoked as soon as possible after to cause a state change. + /// + void EndChange(); + } +} From ed4ce54ac3e7149b3a6f3d4cc9406ef74d2f6e38 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 21:56:36 +0900 Subject: [PATCH 20/77] Add tests --- .../Editor/TestSceneEditorChangeStates.cs | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs new file mode 100644 index 0000000000..abaa373cac --- /dev/null +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs @@ -0,0 +1,193 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editor +{ + public class TestSceneEditorChangeStates : ScreenTestScene + { + private EditorBeatmap editorBeatmap; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + Screens.Edit.Editor editor = null; + + AddStep("load editor", () => + { + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + LoadScreen(editor = new Screens.Edit.Editor()); + }); + + AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType().Single()); + } + + [Test] + public void TestUndoFromInitialState() + { + int hitObjectCount = 0; + + AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); + + addUndoSteps(); + + AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + } + + [Test] + public void TestRedoFromInitialState() + { + int hitObjectCount = 0; + + AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); + + addRedoSteps(); + + AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + } + + [Test] + public void TestAddObjectAndUndo() + { + HitObject addedObject = null; + HitObject removedObject = null; + HitObject expectedObject = null; + + AddStep("bind removal", () => + { + editorBeatmap.HitObjectAdded += h => addedObject = h; + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddAssert("hitobject added", () => addedObject == expectedObject); + + addUndoSteps(); + AddAssert("hitobject removed", () => removedObject == expectedObject); + } + + [Test] + public void TestAddObjectThenUndoThenRedo() + { + HitObject addedObject = null; + HitObject removedObject = null; + HitObject expectedObject = null; + + AddStep("bind removal", () => + { + editorBeatmap.HitObjectAdded += h => addedObject = h; + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + addUndoSteps(); + + AddStep("reset variables", () => + { + addedObject = null; + removedObject = null; + }); + + addRedoSteps(); + AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) + AddAssert("no hitobject removed", () => removedObject == null); + } + + [Test] + public void TestRemoveObjectThenUndo() + { + HitObject addedObject = null; + HitObject removedObject = null; + HitObject expectedObject = null; + + AddStep("bind removal", () => + { + editorBeatmap.HitObjectAdded += h => addedObject = h; + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("remove object", () => editorBeatmap.Remove(expectedObject)); + AddStep("reset variables", () => + { + addedObject = null; + removedObject = null; + }); + + addUndoSteps(); + AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) + AddAssert("no hitobject removed", () => removedObject == null); + } + + [Test] + public void TestRemoveObjectThenUndoThenRedo() + { + HitObject addedObject = null; + HitObject removedObject = null; + HitObject expectedObject = null; + + AddStep("bind removal", () => + { + editorBeatmap.HitObjectAdded += h => addedObject = h; + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("remove object", () => editorBeatmap.Remove(expectedObject)); + addUndoSteps(); + + AddStep("reset variables", () => + { + addedObject = null; + removedObject = null; + }); + + addRedoSteps(); + AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo) + AddAssert("no hitobject added", () => addedObject == null); + } + + private void addUndoSteps() + { + AddStep("press undo", () => + { + InputManager.PressKey(Key.LControl); + InputManager.PressKey(Key.Z); + }); + + AddStep("release keys", () => + { + InputManager.ReleaseKey(Key.LControl); + InputManager.ReleaseKey(Key.Z); + }); + } + + private void addRedoSteps() + { + AddStep("press redo", () => + { + InputManager.PressKey(Key.LControl); + InputManager.PressKey(Key.LShift); + InputManager.PressKey(Key.Z); + }); + + AddStep("release keys", () => + { + InputManager.ReleaseKey(Key.LControl); + InputManager.ReleaseKey(Key.LShift); + InputManager.ReleaseKey(Key.Z); + }); + } + } +} From 575b061dd76a59fd39079904f53193bb524ea261 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 22:00:56 +0900 Subject: [PATCH 21/77] Add change state support to more editor components --- .../Components/PathControlPointPiece.cs | 17 +++++++++++++++- .../Sliders/SliderSelectionBlueprint.cs | 20 +++++++++++++++++-- .../Compose/Components/BlueprintContainer.cs | 14 +++++++++++++ .../Compose/Components/SelectionHandler.cs | 9 ++++++++- .../Timeline/TimelineHitObjectBlueprint.cs | 12 +++++++++-- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index af4da5e853..092a13cca5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private readonly Container marker; private readonly Drawable markerRing; + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + [Resolved(CanBeNull = true)] private IDistanceSnapProvider snapProvider { get; set; } @@ -137,7 +141,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnClick(ClickEvent e) => RequestSelection != null; - protected override bool OnDragStart(DragStartEvent e) => e.Button == MouseButton.Left; + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button == MouseButton.Left) + { + changeHandler?.BeginChange(); + return true; + } + + return false; + } protected override void OnDrag(DragEvent e) { @@ -158,6 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components ControlPoint.Position.Value += e.Delta; } + protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); + /// /// Updates the state of the circular control point marker. /// diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 001100d3ce..b7074b7ee5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -38,6 +38,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private EditorBeatmap editorBeatmap { get; set; } + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + public SliderSelectionBlueprint(DrawableSlider slider) : base(slider) { @@ -92,7 +95,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int? placementControlPointIndex; - protected override bool OnDragStart(DragStartEvent e) => placementControlPointIndex != null; + protected override bool OnDragStart(DragStartEvent e) + { + if (placementControlPointIndex != null) + { + changeHandler?.BeginChange(); + return true; + } + + return false; + } protected override void OnDrag(DragEvent e) { @@ -103,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override void OnDragEnd(DragEndEvent e) { - placementControlPointIndex = null; + if (placementControlPointIndex != null) + { + placementControlPointIndex = null; + changeHandler?.EndChange(); + } } private BindableList controlPoints => HitObject.Path.ControlPoints; diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c81c6059cc..ad16e22e5e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -37,6 +37,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private SelectionHandler selectionHandler; + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + [Resolved] private IAdjustableClock adjustableClock { get; set; } @@ -164,7 +167,11 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; if (movementBlueprint != null) + { + isDraggingBlueprint = true; + changeHandler?.BeginChange(); return true; + } if (DragBox.HandleDrag(e)) { @@ -191,6 +198,12 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return; + if (isDraggingBlueprint) + { + changeHandler?.EndChange(); + isDraggingBlueprint = false; + } + if (DragBox.State == Visibility.Visible) { DragBox.Hide(); @@ -354,6 +367,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private Vector2? movementBlueprintOriginalPosition; private SelectionBlueprint movementBlueprint; + private bool isDraggingBlueprint; /// /// Attempts to begin the movement of any selected blueprints. diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index fc46bf3fed..e212979433 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -40,6 +40,9 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved(CanBeNull = true)] private EditorBeatmap editorBeatmap { get; set; } + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + public SelectionHandler() { selectedBlueprints = new List(); @@ -152,8 +155,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void deleteSelected() { + changeHandler?.BeginChange(); + foreach (var h in selectedBlueprints.ToList()) - editorBeatmap.Remove(h.HitObject); + editorBeatmap?.Remove(h.HitObject); + + changeHandler?.EndChange(); } #endregion diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 8f12c2f0ed..16ba3ba89a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -254,14 +254,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White; } - protected override bool OnDragStart(DragStartEvent e) => true; - [Resolved] private EditorBeatmap beatmap { get; set; } [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + + protected override bool OnDragStart(DragStartEvent e) + { + changeHandler?.BeginChange(); + return true; + } + protected override void OnDrag(DragEvent e) { base.OnDrag(e); @@ -301,6 +308,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline base.OnDragEnd(e); OnDragHandled?.Invoke(null); + changeHandler?.EndChange(); } } } From e208251fc6f1729eac62cfa2e7e756b651eedbc4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 23:18:43 +0900 Subject: [PATCH 22/77] Wait for timeline to also load --- osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs index abaa373cac..dd1b6cf6aa 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK.Input; namespace osu.Game.Tests.Visual.Editor @@ -29,7 +30,8 @@ namespace osu.Game.Tests.Visual.Editor LoadScreen(editor = new Screens.Edit.Editor()); }); - AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType().Single()); } From 2201e9b4ae00154d14a4b0ccb45b881550c7bdfb Mon Sep 17 00:00:00 2001 From: Fire937 Date: Thu, 9 Apr 2020 18:12:15 +0200 Subject: [PATCH 23/77] Add stereo shifted hitsound playback support There is now a setting in the general settings called "Positional hitsounds". If the setting is enabled, the hitsounds playback will be shifted according to their position on the beatmap. --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../Sections/Gameplay/GeneralSettings.cs | 5 ++++ .../Objects/Drawables/DrawableHitObject.cs | 26 ++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 41f6747b74..ab5a652a94 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -88,6 +88,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowProgressGraph, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); Set(OsuSetting.KeyOverlay, false); + Set(OsuSetting.PositionalHitSounds, true); Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); Set(OsuSetting.FloatingComments, false); @@ -176,6 +177,7 @@ namespace osu.Game.Configuration LightenDuringBreaks, ShowStoryboard, KeyOverlay, + PositionalHitSounds, ScoreMeter, FloatingComments, ShowInterface, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 2d2cd42213..ef03c0622a 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -57,6 +57,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Always show key overlay", Bindable = config.GetBindable(OsuSetting.KeyOverlay) }, + new SettingsCheckbox + { + LabelText = "Positional hitsounds", + Bindable = config.GetBindable(OsuSetting.PositionalHitSounds) + }, new SettingsEnumDropdown { LabelText = "Score meter type", diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0011faefbb..ed9efba89f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -12,11 +12,13 @@ using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Threading; +using osu.Framework.Audio; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using osu.Game.Configuration; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -31,6 +33,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public readonly Bindable AccentColour = new Bindable(Color4.Gray); + /// + /// The stereo balance of the samples if the Positional hitsounds setting is set. + /// + private readonly BindableDouble positionalSoundAdjustment = new BindableDouble(); + protected SkinnableSound Samples { get; private set; } protected virtual IEnumerable GetSamples() => HitObject.Samples; @@ -84,8 +91,14 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public JudgementResult Result { get; private set; } + /// + /// The stereo balance of the samples played if Positional hitsounds is set. + /// + protected virtual float PositionalSound => (HitObject is IHasXPosition position) ? (position.X / 512f - 0.5f) * 0.8f : 0; + private BindableList samplesBindable; private Bindable startTimeBindable; + private Bindable userPositionalHitSounds; private Bindable comboIndexBindable; public override bool RemoveWhenNotAlive => false; @@ -101,10 +114,11 @@ namespace osu.Game.Rulesets.Objects.Drawables protected DrawableHitObject([NotNull] HitObject hitObject) { HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject)); + positionalSoundAdjustment.Value = PositionalSound; } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { var judgement = HitObject.CreateJudgement(); @@ -113,6 +127,16 @@ namespace osu.Game.Rulesets.Objects.Drawables throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); loadSamples(); + + userPositionalHitSounds = config.GetBindable(OsuSetting.PositionalHitSounds); + userPositionalHitSounds.BindValueChanged(positional => + { + if (positional.NewValue) + Samples?.AddAdjustment(AdjustableProperty.Balance, positionalSoundAdjustment); + else + Samples?.RemoveAdjustment(AdjustableProperty.Balance, positionalSoundAdjustment); + }); + userPositionalHitSounds.TriggerChange(); } protected override void LoadComplete() From 116b952dfe973218621de51532c8620c0f65e015 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:20:43 +0900 Subject: [PATCH 24/77] Change param to hitobject rather than result --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index db8a47e4a2..9c066c367b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { - missAllEarlier(result); + missAllEarlier(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -131,14 +131,14 @@ namespace osu.Game.Rulesets.Osu.UI /// /// Misses all s occurring earlier than the start time of a judged . /// - /// The of the judged . - private void missAllEarlier(JudgementResult result) + /// The marker , which all s earlier than will get missed. + private void missAllEarlier(HitObject hitObject) { - if (!causesNoteLockMisses(result.HitObject)) + if (!causesNoteLockMisses(hitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. - double minimumTime = result.HitObject.StartTime; + double minimumTime = hitObject.StartTime; foreach (var obj in HitObjectContainer.AliveObjects) { From b8d7b78b55a3022e8556110a44bc4d40c977c86e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:21:37 +0900 Subject: [PATCH 25/77] Remove unnecessary null set --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9c066c367b..9011f21fd5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -78,12 +78,7 @@ namespace osu.Game.Rulesets.Osu.UI bool result = base.Remove(h); if (result) - { - DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; - osuHitObject.CheckHittable = null; - - followPoints.RemoveFollowPoints(osuHitObject); - } + followPoints.RemoveFollowPoints((DrawableOsuHitObject)h); return result; } From ea1bec85ae7ef875f133add73c5051a6fa9b5a4c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:40:20 +0900 Subject: [PATCH 26/77] Simplify code/language --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 65 +++++++++--------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9011f21fd5..f4009a281c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; @@ -87,10 +86,10 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject lastObject = osuHitObject; - // Get the last hitobject that contributes to note lock + // Get the last hitobject that can block future hits while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) { - if (contributesToNoteLock(lastObject.HitObject)) + if (canBlockFutureHits(lastObject.HitObject)) break; } @@ -108,7 +107,9 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { - missAllEarlier(result.HitObject); + // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. + if (canBlockFutureHits(result.HitObject)) + missAllEarlierObjects(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -127,12 +128,8 @@ namespace osu.Game.Rulesets.Osu.UI /// Misses all s occurring earlier than the start time of a judged . /// /// The marker , which all s earlier than will get missed. - private void missAllEarlier(HitObject hitObject) + private void missAllEarlierObjects(HitObject hitObject) { - if (!causesNoteLockMisses(hitObject)) - return; - - // The minimum start time required for hitobjects so that they aren't missed. double minimumTime = hitObject.StartTime; foreach (var obj in HitObjectContainer.AliveObjects) @@ -140,50 +137,36 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - performMiss(obj); - - foreach (var n in obj.NestedHitObjects) + switch (obj) { - if (n.HitObject.StartTime >= minimumTime) + case DrawableHitCircle circle: + miss(circle); break; - performMiss(n); + case DrawableSlider slider: + miss(slider.HeadCircle); + break; } } + + static void miss(DrawableOsuHitObject obj) + { + // Hitobjects that have already been judged cannot be missed. + if (obj.Judged) + return; + + obj.MissForcefully(); + } } - private void performMiss(DrawableHitObject obj) - { - if (!(obj is DrawableOsuHitObject osuObject)) - throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); - - // Hitobjects that have already been judged cannot be missed. - if (osuObject.Judged) - return; - - if (!causesNoteLockMisses(obj.HitObject)) - return; - - osuObject.MissForcefully(); - } - /// - /// Whether a is contributes to note lock. - /// Future contributing s will not be hittable until the start time of the last contributing is reached. + /// Whether a can block hits on future s until its start time is reached. /// /// The to test. - /// Whether causes note lock. - private bool contributesToNoteLock(HitObject hitObject) + /// Whether can block hits on future s. + private bool canBlockFutureHits(HitObject hitObject) => hitObject is HitCircle || hitObject is Slider; - /// - /// Whether a can be missed and causes other s to be missed when hit out-of-order during note lock. - /// - /// The to test. - /// Whether contributes to note lock misses. - private bool causesNoteLockMisses(HitObject hitObject) - => hitObject is HitCircle && !(hitObject is SliderTailCircle); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From 10e849d19616d3fa1314e8fa81ea10e12111e1da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:02:09 +0900 Subject: [PATCH 27/77] Separate into separate class --- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 2 +- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 104 ++++++++++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 76 +------------ 4 files changed, 111 insertions(+), 73 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 5776c64c86..d73ad888f4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None || CheckHittable?.Invoke(this) == false) + if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false) { Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss)); return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 13829dc2f7..fe23e3729d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Whether this can be hit. /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// - public Func CheckHittable; + public Func CheckHittable; protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs new file mode 100644 index 0000000000..ddaf714e5b --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -0,0 +1,104 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// Ensures that s are hit in-order. + /// If a is hit out of order: + /// + /// The hit is blocked if it occurred earlier than the previous 's start time. + /// The hit causes all previous s to missed otherwise. + /// + /// + public class OrderedHitPolicy + { + private readonly HitObjectContainer hitObjectContainer; + + public OrderedHitPolicy(HitObjectContainer hitObjectContainer) + { + this.hitObjectContainer = hitObjectContainer; + } + + /// + /// Determines whether a can be hit at a point in time. + /// + /// The to check. + /// The time to check. + /// Whether can be hit at the given . + public bool IsHittable(DrawableHitObject hitObject, double time) + { + DrawableHitObject lastObject = hitObject; + + // Get the last hitobject that can block future hits + while ((lastObject = hitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + { + if (canBlockFutureHits(lastObject.HitObject)) + break; + } + + // If there is no previous object alive, allow the hit. + if (lastObject == null) + return true; + + // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. + // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. + if (lastObject.Judged || time >= lastObject.HitObject.StartTime) + return true; + + return false; + } + + /// + /// Handles a being hit to potentially miss all earlier s. + /// + /// The that was hit. + public void HandleHit(HitObject hitObject) + { + if (!canBlockFutureHits(hitObject)) + return; + + double minimumTime = hitObject.StartTime; + + foreach (var obj in hitObjectContainer.AliveObjects) + { + if (obj.HitObject.StartTime >= minimumTime) + break; + + switch (obj) + { + case DrawableHitCircle circle: + miss(circle); + break; + + case DrawableSlider slider: + miss(slider.HeadCircle); + break; + } + } + + static void miss(DrawableOsuHitObject obj) + { + // Hitobjects that have already been judged cannot be missed. + if (obj.Judged) + return; + + obj.MissForcefully(); + } + } + + /// + /// Whether a blocks hits on future s until its start time is reached. + /// + /// The to test. + private bool canBlockFutureHits(HitObject hitObject) + => hitObject is HitCircle || hitObject is Slider; + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index f4009a281c..2f222f59b4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,7 +10,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -22,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ApproachCircleProxyContainer approachCircles; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; + private readonly OrderedHitPolicy hitPolicy; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -53,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.UI Depth = -1, }, }; + + hitPolicy = new OrderedHitPolicy(HitObjectContainer); } public override void Add(DrawableHitObject h) @@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI base.Add(h); DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; - osuHitObject.CheckHittable = checkHittable; + osuHitObject.CheckHittable = hitPolicy.IsHittable; followPoints.AddFollowPoints(osuHitObject); } @@ -82,34 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI return result; } - private bool checkHittable(DrawableOsuHitObject osuHitObject) - { - DrawableHitObject lastObject = osuHitObject; - - // Get the last hitobject that can block future hits - while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) - { - if (canBlockFutureHits(lastObject.HitObject)) - break; - } - - // If there is no previous object alive, allow the hit. - if (lastObject == null) - return true; - - // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. - // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. - if (lastObject.Judged || Time.Current >= lastObject.HitObject.StartTime) - return true; - - return false; - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. - if (canBlockFutureHits(result.HitObject)) - missAllEarlierObjects(result.HitObject); + hitPolicy.HandleHit(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -124,49 +101,6 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer.Add(explosion); } - /// - /// Misses all s occurring earlier than the start time of a judged . - /// - /// The marker , which all s earlier than will get missed. - private void missAllEarlierObjects(HitObject hitObject) - { - double minimumTime = hitObject.StartTime; - - foreach (var obj in HitObjectContainer.AliveObjects) - { - if (obj.HitObject.StartTime >= minimumTime) - break; - - switch (obj) - { - case DrawableHitCircle circle: - miss(circle); - break; - - case DrawableSlider slider: - miss(slider.HeadCircle); - break; - } - } - - static void miss(DrawableOsuHitObject obj) - { - // Hitobjects that have already been judged cannot be missed. - if (obj.Judged) - return; - - obj.MissForcefully(); - } - } - - /// - /// Whether a can block hits on future s until its start time is reached. - /// - /// The to test. - /// Whether can block hits on future s. - private bool canBlockFutureHits(HitObject hitObject) - => hitObject is HitCircle || hitObject is Slider; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From b54bbc5f6a217352a03ed77eb05eb20e50a948fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:41:37 +0900 Subject: [PATCH 28/77] Improve commenting + refactor --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 79 +++++++++++++------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index ddaf714e5b..0a09b5be7c 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -35,22 +34,27 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether can be hit at the given . public bool IsHittable(DrawableHitObject hitObject, double time) { - DrawableHitObject lastObject = hitObject; + DrawableHitObject blockingObject = null; - // Get the last hitobject that can block future hits - while ((lastObject = hitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + // Find the last hitobject which blocks future hits. + foreach (var obj in hitObjectContainer.AliveObjects) { - if (canBlockFutureHits(lastObject.HitObject)) + if (obj == hitObject) break; + + if (canBlockFutureHits(obj)) + blockingObject = obj; } - // If there is no previous object alive, allow the hit. - if (lastObject == null) + // If there is no previous hitobject, allow the hit. + if (blockingObject == null) return true; - // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. - // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. - if (lastObject.Judged || time >= lastObject.HitObject.StartTime) + // A hit is allowed if: + // 1. The last blocking hitobject has been judged. + // 2. The current time is after the last hitobject's start time. + // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245). + if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) return true; return false; @@ -62,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks) if (!canBlockFutureHits(hitObject)) return; @@ -72,33 +77,57 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - switch (obj) + // If the parent hitobject cannot cause a miss, neither can any nested hitobject. + if (!canBlockFutureHits(obj)) + continue; + + applyMiss(obj); + + foreach (var nested in obj.NestedHitObjects) { - case DrawableHitCircle circle: - miss(circle); + if (nested.HitObject.StartTime >= minimumTime) break; - case DrawableSlider slider: - miss(slider.HeadCircle); - break; + if (canBlockFutureHits(nested)) + applyMiss(nested); } } - static void miss(DrawableOsuHitObject obj) - { - // Hitobjects that have already been judged cannot be missed. - if (obj.Judged) - return; + static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully(); + } - obj.MissForcefully(); - } + /// + /// Whether a blocks hits on future s until its start time is reached. + /// + /// + /// Must only be used when iterating through top-most drawable hitobjects. + /// + /// The to test. + private static bool canBlockFutureHits(DrawableHitObject hitObject) + { + // Judged hitobjects can never block hits. + if (hitObject.Judged) + return false; + + // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. + return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } /// /// Whether a blocks hits on future s until its start time is reached. /// + /// + /// Must only be used when iterating through nested hitobjects. + /// /// The to test. - private bool canBlockFutureHits(HitObject hitObject) - => hitObject is HitCircle || hitObject is Slider; + private static bool canBlockFutureHits(HitObject hitObject) + { + // Unlike the above we will receive slider tails, but they do not block future hits. + if (hitObject is SliderTailCircle) + return false; + + // All other hitcircles continue to block future hits. + return hitObject is HitCircle; + } } } From 42b3ff805b60c740fbd85948a8db366f8e91952b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:57:31 +0900 Subject: [PATCH 29/77] Rename methods + fix incorrect method usage --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 33 ++++++++------------ 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 0a09b5be7c..cfb850b785 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI if (obj == hitObject) break; - if (canBlockFutureHits(obj)) + if (drawableCanBlockFutureHits(obj)) blockingObject = obj; } @@ -66,29 +66,26 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { - // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks) - if (!canBlockFutureHits(hitObject)) + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners) + if (!hitObjectCanBlockFutureHits(hitObject)) return; double minimumTime = hitObject.StartTime; foreach (var obj in hitObjectContainer.AliveObjects) { - if (obj.HitObject.StartTime >= minimumTime) - break; - - // If the parent hitobject cannot cause a miss, neither can any nested hitobject. - if (!canBlockFutureHits(obj)) + if (obj.Judged || obj.HitObject.StartTime >= minimumTime) continue; - applyMiss(obj); + if (hitObjectCanBlockFutureHits(obj.HitObject)) + applyMiss(obj); foreach (var nested in obj.NestedHitObjects) { - if (nested.HitObject.StartTime >= minimumTime) - break; + if (nested.Judged || nested.HitObject.StartTime >= minimumTime) + continue; - if (canBlockFutureHits(nested)) + if (hitObjectCanBlockFutureHits(nested.HitObject)) applyMiss(nested); } } @@ -100,15 +97,11 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether a blocks hits on future s until its start time is reached. /// /// - /// Must only be used when iterating through top-most drawable hitobjects. + /// This will ONLY match on top-most s. /// /// The to test. - private static bool canBlockFutureHits(DrawableHitObject hitObject) + private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject) { - // Judged hitobjects can never block hits. - if (hitObject.Judged) - return false; - // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } @@ -117,10 +110,10 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether a blocks hits on future s until its start time is reached. /// /// - /// Must only be used when iterating through nested hitobjects. + /// This is more rigorous and may not match on top-most s as does. /// /// The to test. - private static bool canBlockFutureHits(HitObject hitObject) + private static bool hitObjectCanBlockFutureHits(HitObject hitObject) { // Unlike the above we will receive slider tails, but they do not block future hits. if (hitObject is SliderTailCircle) From 15a92d1451c9fe0fb916f4c4e392c4da2411dcf3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:57:35 +0900 Subject: [PATCH 30/77] Rename test scene --- .../{TestSceneNoteLock.cs => TestSceneOutOfOrderHits.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Rulesets.Osu.Tests/{TestSceneNoteLock.cs => TestSceneOutOfOrderHits.cs} (99%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 2c69540951..d6858f831e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -25,7 +25,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneNoteLock : RateAdjustedBeatmapTestScene + public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene { private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss From 6988df30bd9cdfe7be77a21f6de63b11ca462a45 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 03:12:13 +0900 Subject: [PATCH 31/77] Rename variable, add comment --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index cfb850b785..dfca2aff7b 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -66,15 +66,16 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { - // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners) + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners). if (!hitObjectCanBlockFutureHits(hitObject)) return; - double minimumTime = hitObject.StartTime; + double maximumTime = hitObject.StartTime; + // Iterate through and apply miss results to all top-level and nested hitobjects which block future hits. foreach (var obj in hitObjectContainer.AliveObjects) { - if (obj.Judged || obj.HitObject.StartTime >= minimumTime) + if (obj.Judged || obj.HitObject.StartTime >= maximumTime) continue; if (hitObjectCanBlockFutureHits(obj.HitObject)) @@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.UI foreach (var nested in obj.NestedHitObjects) { - if (nested.Judged || nested.HitObject.StartTime >= minimumTime) + if (nested.Judged || nested.HitObject.StartTime >= maximumTime) continue; if (hitObjectCanBlockFutureHits(nested.HitObject)) From c17e47026623afde64aaf11b8334b5ebcd221696 Mon Sep 17 00:00:00 2001 From: Fire937 Date: Fri, 10 Apr 2020 00:01:35 +0200 Subject: [PATCH 32/77] Fix PositionalSound calculation implementation The position used to calculate the stereo balance is now the position of the drawable (as opposed to the position specified in the beatmap file previously). --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ed9efba89f..30a9106ddc 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// The stereo balance of the samples played if Positional hitsounds is set. /// - protected virtual float PositionalSound => (HitObject is IHasXPosition position) ? (position.X / 512f - 0.5f) * 0.8f : 0; + protected virtual float PositionalSound => (Position.X / 512f - 0.5f) * 0.8f; private BindableList samplesBindable; private Bindable startTimeBindable; @@ -114,7 +114,6 @@ namespace osu.Game.Rulesets.Objects.Drawables protected DrawableHitObject([NotNull] HitObject hitObject) { HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject)); - positionalSoundAdjustment.Value = PositionalSound; } [BackgroundDependencyLoader] @@ -377,7 +376,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. /// - public virtual void PlaySamples() => Samples?.Play(); + public virtual void PlaySamples() + { + positionalSoundAdjustment.Value = PositionalSound; + Samples?.Play(); + } protected override void Update() { From 4a87ac784061318026fe650a48e42ca455e7e7f9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 13:53:09 +0900 Subject: [PATCH 33/77] Add support for sample changes --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index e212979433..764eae1056 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -212,6 +212,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void AddHitSample(string sampleName) { + changeHandler?.BeginChange(); + foreach (var h in SelectedHitObjects) { // Make sure there isn't already an existing sample @@ -220,6 +222,8 @@ namespace osu.Game.Screens.Edit.Compose.Components h.Samples.Add(new HitSampleInfo { Name = sampleName }); } + + changeHandler?.EndChange(); } /// @@ -228,8 +232,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { + changeHandler?.BeginChange(); + foreach (var h in SelectedHitObjects) h.SamplesBindable.RemoveAll(s => s.Name == sampleName); + + changeHandler?.EndChange(); } #endregion From 7fba29113466d5fe6aa43eef0cd284323f1c969a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 13:14:34 +0900 Subject: [PATCH 34/77] Change inheritance of taiko hit pieces to better allow for skinning --- .../TestSceneDrawableHit.cs | 54 ++++++++++++++++++ .../Objects/Drawables/DrawableCentreHit.cs | 10 +--- .../Objects/Drawables/DrawableDrumRoll.cs | 10 ++-- .../Objects/Drawables/DrawableDrumRollTick.cs | 3 +- .../Objects/Drawables/DrawableRimHit.cs | 10 +--- .../Objects/Drawables/DrawableSwell.cs | 16 +++--- .../Objects/Drawables/DrawableSwellTick.cs | 4 ++ .../Drawables/DrawableTaikoHitObject.cs | 14 ++--- .../Drawables/Pieces/CentreHitSymbolPiece.cs | 50 +++++++++++------ .../Drawables/Pieces/RimHitCirclePiece.cs | 55 +++++++++++++++++++ .../Drawables/Pieces/RimHitSymbolPiece.cs | 39 ------------- .../Drawables/Pieces/SwellSymbolPiece.cs | 50 +++++++++++------ .../Objects/Drawables/Pieces/TickPiece.cs | 6 +- 13 files changed, 209 insertions(+), 112 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs create mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs delete mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs new file mode 100644 index 0000000000..b927f0294b --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TestSceneDrawableHit : TaikoSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(DrawableHit), + typeof(DrawableCentreHit), + typeof(DrawableRimHit), + }).ToList(); + + [BackgroundDependencyLoader] + private void load() + { + AddStep("Centre hit", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + } + + private Hit createHitAtCurrentTime() + { + var hit = new Hit + { + StartTime = Time.Current + 3000, + }; + + hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + return hit; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 4979135f50..22d62442cf 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -1,8 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -14,13 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public DrawableCentreHit(Hit hit) : base(hit) { - MainPiece.Add(new CentreHitSymbolPiece()); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - MainPiece.AccentColour = colours.PinkDarker; - } + protected override CompositeDrawable CreateMainPiece() => new CentreHitCirclePiece(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 5806c90115..0627eb95fd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -34,17 +34,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Color4 colourIdle; private Color4 colourEngaged; + private ElongatedCirclePiece elongatedPiece; + public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) { RelativeSizeAxes = Axes.Y; - MainPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); + elongatedPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - MainPiece.AccentColour = colourIdle = colours.YellowDark; + elongatedPiece.AccentColour = colourIdle = colours.YellowDark; colourEngaged = colours.YellowDarker; } @@ -84,7 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return base.CreateNestedHitObject(hitObject); } - protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece(); + protected override CompositeDrawable CreateMainPiece() => elongatedPiece = new ElongatedCirclePiece(); public override bool OnPressed(TaikoAction action) => false; @@ -101,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour); Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); - MainPiece.FadeAccent(newColour, 100); + (MainPiece as IHasAccentColour)?.FadeAccent(newColour, 100); } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 25b6141a0e..fea3eea6a9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; @@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool DisplayResult => false; - protected override TaikoPiece CreateMainPiece() => new TickPiece + protected override CompositeDrawable CreateMainPiece() => new TickPiece { Filled = HitObject.FirstTick }; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 5a12d71cea..6dad7af907 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -1,8 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -14,13 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public DrawableRimHit(Hit hit) : base(hit) { - MainPiece.Add(new RimHitSymbolPiece()); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - MainPiece.AccentColour = colours.BlueDarker; - } + protected override CompositeDrawable CreateMainPiece() => new RimHitCirclePiece(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index fa39819199..3a2e44038f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -9,11 +9,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; - private readonly SwellSymbolPiece symbol; - public DrawableSwell(Swell swell) : base(swell) { @@ -107,18 +105,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables }); AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); - - MainPiece.Add(symbol = new SwellSymbolPiece()); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - MainPiece.AccentColour = colours.YellowDark; expandingRing.Colour = colours.YellowLight; targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } + protected override CompositeDrawable CreateMainPiece() => new SwellCirclePiece + { + // to allow for rotation transform + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + protected override void LoadComplete() { base.LoadComplete(); @@ -182,7 +184,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables .Then() .FadeTo(completion / 8, 2000, Easing.OutQuint); - symbol.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); + MainPiece.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index ce875ebba8..5a954addfb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -28,5 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } public override bool OnPressed(TaikoAction action) => false; + + protected override CompositeDrawable CreateMainPiece() => new TickPiece(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 5f892dd2fa..397888bb11 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -4,7 +4,6 @@ using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osuTK; using System.Linq; using osu.Game.Audio; @@ -108,19 +107,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject - where TTaikoHit : TaikoHitObject + public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject + where TObject : TaikoHitObject { public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); - public new TTaikoHit HitObject; + public new TObject HitObject; protected readonly Vector2 BaseSize; - protected readonly TaikoPiece MainPiece; + protected readonly CompositeDrawable MainPiece; private readonly Container strongHitContainer; - protected DrawableTaikoHitObject(TTaikoHit hitObject) + protected DrawableTaikoHitObject(TObject hitObject) : base(hitObject) { HitObject = hitObject; @@ -132,7 +131,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); Content.Add(MainPiece = CreateMainPiece()); - MainPiece.KiaiMode = HitObject.Kiai; AddInternal(strongHitContainer = new Container()); } @@ -169,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // Normal and clap samples are handled by the drum protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); - protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); + protected abstract CompositeDrawable CreateMainPiece(); /// /// Creates the handler for this 's . diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs index 7ed61ede96..0509841ba8 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs @@ -1,36 +1,52 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { - /// - /// The symbol used for centre hit pieces. - /// - public class CentreHitSymbolPiece : Container + public class CentreHitCirclePiece : CirclePiece { - public CentreHitSymbolPiece() + public CentreHitCirclePiece() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Add(new CentreHitSymbolPiece()); + } - RelativeSizeAxes = Axes.Both; - Size = new Vector2(CirclePiece.SYMBOL_SIZE); - Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER); + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.PinkDarker; + } - Children = new[] + /// + /// The symbol used for centre hit pieces. + /// + public class CentreHitSymbolPiece : Container + { + public CentreHitSymbolPiece() { - new CircularContainer + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(SYMBOL_SIZE); + Padding = new MarginPadding(SYMBOL_BORDER); + + Children = new[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] { new Box { RelativeSizeAxes = Axes.Both } } - } - }; + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new[] { new Box { RelativeSizeAxes = Axes.Both } } + } + }; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs new file mode 100644 index 0000000000..3273ab7fa7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces +{ + public class RimHitCirclePiece : CirclePiece + { + public RimHitCirclePiece() + { + Add(new RimHitSymbolPiece()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.BlueDarker; + } + + /// + /// The symbol used for rim hit pieces. + /// + public class RimHitSymbolPiece : CircularContainer + { + public RimHitSymbolPiece() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(SYMBOL_SIZE); + + BorderThickness = SYMBOL_BORDER; + BorderColour = Color4.White; + Masking = true; + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs deleted file mode 100644 index e4c964a884..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces -{ - /// - /// The symbol used for rim hit pieces. - /// - public class RimHitSymbolPiece : CircularContainer - { - public RimHitSymbolPiece() - { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - - RelativeSizeAxes = Axes.Both; - Size = new Vector2(CirclePiece.SYMBOL_SIZE); - - BorderThickness = CirclePiece.SYMBOL_BORDER; - BorderColour = Color4.White; - Masking = true; - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - }; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs index 0ed9923924..a8f9f0b94d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs @@ -1,36 +1,52 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { - /// - /// The symbol used for swell pieces. - /// - public class SwellSymbolPiece : Container + public class SwellCirclePiece : CirclePiece { - public SwellSymbolPiece() + public SwellCirclePiece() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Add(new SwellSymbolPiece()); + } - RelativeSizeAxes = Axes.Both; - Size = new Vector2(CirclePiece.SYMBOL_SIZE); - Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER); + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.YellowDark; + } - Children = new[] + /// + /// The symbol used for swell pieces. + /// + public class SwellSymbolPiece : Container + { + public SwellSymbolPiece() { - new SpriteIcon + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(SYMBOL_SIZE); + Padding = new MarginPadding(SYMBOL_BORDER); + + Children = new[] { - RelativeSizeAxes = Axes.Both, - Icon = FontAwesome.Solid.Asterisk, - Shadow = false - } - }; + new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Asterisk, + Shadow = false + } + }; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs index 83cf7a64ec..0648bcebcd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { - public class TickPiece : TaikoPiece + public class TickPiece : CompositeDrawable { /// /// Any tick that is not the first for a drumroll is not filled, but is instead displayed @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces FillMode = FillMode.Fit; Size = new Vector2(tick_size); - Add(new CircularContainer + InternalChild = new CircularContainer { RelativeSizeAxes = Axes.Both, Masking = true, @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces AlwaysPresent = true } } - }); + }; } } } From ca2df77c7684899cc107d72f646e90461244a8e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 13:14:41 +0900 Subject: [PATCH 35/77] Add default skin test resources --- .../metrics-skin/taikohitcircle@2x.png | Bin 0 -> 13140 bytes .../metrics-skin/taikohitcircleoverlay@2x.png | Bin 0 -> 38217 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..043bfbfae1a77317aec68051b2d917a42d143fb1 GIT binary patch literal 13140 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 R|M{Pbx-w?>O~E?q{{RqUO*sGn literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4233d9bb6e6aa88c7da5c1998439789cc5f9c7b2 GIT binary patch literal 38217 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 literal 0 HcmV?d00001 From 78db83fd0e88ad1217f96ff83cb077c800ad9494 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 13:33:19 +0900 Subject: [PATCH 36/77] Remove TaikoPiece class and localise kiai for now --- ...SymbolPiece.cs => CentreHitCirclePiece.cs} | 0 .../Objects/Drawables/Pieces/CirclePiece.cs | 19 ++++++++----- .../Objects/Drawables/Pieces/TaikoPiece.cs | 28 ------------------- 3 files changed, 12 insertions(+), 35 deletions(-) rename osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/{CentreHitSymbolPiece.cs => CentreHitCirclePiece.cs} (100%) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs rename to osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index d9c0664ecd..ce2882656a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -10,6 +10,7 @@ using osuTK.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Effects; +using osu.Game.Graphics.Containers; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { @@ -20,21 +21,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces /// for a usage example. /// /// - public class CirclePiece : TaikoPiece + public class CirclePiece : BeatSyncedContainer { public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_BORDER = 8; private const double pre_beat_transition_time = 80; + private Color4 accentColour; + /// /// The colour of the inner circle and outer glows. /// - public override Color4 AccentColour + public Color4 AccentColour { - get => base.AccentColour; + get => accentColour; set { - base.AccentColour = value; + accentColour = value; background.Colour = AccentColour; @@ -42,15 +45,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces } } + private bool kiaiMode; + /// /// Whether Kiai mode effects are enabled for this circle piece. /// - public override bool KiaiMode + public bool KiaiMode { - get => base.KiaiMode; + get => kiaiMode; set { - base.KiaiMode = value; + kiaiMode = value; resetEdgeEffects(); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs deleted file mode 100644 index 8067054f8f..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; -using osuTK.Graphics; -using osu.Game.Graphics.Containers; -using osu.Framework.Graphics; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces -{ - public class TaikoPiece : BeatSyncedContainer, IHasAccentColour - { - /// - /// The colour of the inner circle and outer glows. - /// - public virtual Color4 AccentColour { get; set; } - - /// - /// Whether Kiai mode effects are enabled for this circle piece. - /// - public virtual bool KiaiMode { get; set; } - - public TaikoPiece() - { - RelativeSizeAxes = Axes.Both; - } - } -} From 009b1383648dd267e76ee13f72daac2ee1bb1458 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 13:41:14 +0900 Subject: [PATCH 37/77] Prepare for skinnable versions --- .../Objects/Drawables/DrawableCentreHit.cs | 4 +++- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs | 4 +++- .../Objects/Drawables/Pieces/CirclePiece.cs | 2 ++ osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 2 ++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 22d62442cf..f3f4c59a62 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override CompositeDrawable CreateMainPiece() => new CentreHitCirclePiece(); + protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), + _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 6dad7af907..463a8b746c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override CompositeDrawable CreateMainPiece() => new RimHitCirclePiece(); + protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), + _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index ce2882656a..70fe4b7bb2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -71,6 +71,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public CirclePiece() { + RelativeSizeAxes = Axes.Both; + EarlyActivationMilliseconds = pre_beat_transition_time; AddRangeInternal(new Drawable[] diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 6d4581db80..babf21b6a9 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -6,5 +6,7 @@ namespace osu.Game.Rulesets.Taiko public enum TaikoSkinComponents { InputDrum, + CentreHit, + RimHit } } From dc56be0a1d946d08acede69fc6619dcc47298e3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 14:20:09 +0900 Subject: [PATCH 38/77] Add support for skinned hits --- .../TestSceneDrawableHit.cs | 2 + osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 62 +++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 8 +++ 3 files changed, 72 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs index b927f0294b..f2198031db 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Skinning; namespace osu.Game.Rulesets.Taiko.Tests { @@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests typeof(DrawableHit), typeof(DrawableCentreHit), typeof(DrawableRimHit), + typeof(LegacyHit), }).ToList(); [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs new file mode 100644 index 0000000000..bb76eac865 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class LegacyHit : CompositeDrawable, IHasAccentColour + { + private readonly TaikoSkinComponents component; + + private Drawable backgroundLayer; + + public LegacyHit(TaikoSkinComponents component) + { + this.component = component; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + InternalChildren = new Drawable[] + { + backgroundLayer = skin.GetAnimation("taikohitcircle", true, false), + skin.GetAnimation("taikohitcircleoverlay", true, false), + }; + + // animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). + // for now just stop at first frame for sanity. + foreach (var c in InternalChildren) + { + (c as IFramedAnimation)?.Stop(); + c.Anchor = Anchor.Centre; + c.Origin = Anchor.Centre; + } + + AccentColour = component == TaikoSkinComponents.CentreHit + ? new Color4(235, 69, 44, 255) + : new Color4(67, 142, 172, 255); + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (value == accentColour) + return; + + backgroundLayer.Colour = accentColour = value; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 78eec94590..9cd625c35f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -32,6 +32,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning return new LegacyInputDrum(); return null; + + case TaikoSkinComponents.CentreHit: + case TaikoSkinComponents.RimHit: + + if (GetTexture("taikohitcircle") != null) + return new LegacyHit(taikoComponent.Component); + + return null; } return source.GetDrawableComponent(component); From 96bf86099c89444e5328adaa3c169a60d47854c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 14:43:57 +0900 Subject: [PATCH 39/77] Fix scaling of strong hits --- .../TestSceneDrawableHit.cs | 16 +++++++++++++++- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs index f2198031db..301295253d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs @@ -34,17 +34,31 @@ namespace osu.Game.Rulesets.Taiko.Tests Anchor = Anchor.Centre, Origin = Anchor.Centre, })); + + AddStep("Centre hit (strong)", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, })); + + AddStep("Rim hit (strong)", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); } - private Hit createHitAtCurrentTime() + private Hit createHitAtCurrentTime(bool strong = false) { var hit = new Hit { + IsStrong = strong, StartTime = Time.Current + 3000, }; diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index bb76eac865..af10944ee9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning @@ -20,12 +21,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning public LegacyHit(TaikoSkinComponents component) { this.component = component; + + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(ISkinSource skin) { - InternalChildren = new Drawable[] + InternalChildren = new[] { backgroundLayer = skin.GetAnimation("taikohitcircle", true, false), skin.GetAnimation("taikohitcircleoverlay", true, false), @@ -36,6 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning foreach (var c in InternalChildren) { (c as IFramedAnimation)?.Stop(); + c.Anchor = Anchor.Centre; c.Origin = Anchor.Centre; } @@ -45,6 +49,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning : new Color4(67, 142, 172, 255); } + protected override void Update() + { + base.Update(); + + // not all skins (including the default osu-stable) have similar sizes for hitcircle and hitcircleoverlay. + // this ensures they are scaled relative to each other but also match the expected DrawableHit size. + foreach (var c in InternalChildren) + c.Scale = new Vector2(DrawWidth / 128); + } + private Color4 accentColour; public Color4 AccentColour From bf938a37e3d374075e0b9f8e2231e41dc2297949 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 14:51:27 +0900 Subject: [PATCH 40/77] Add old skin test resources (with "animation") --- .../Resources/old-skin/taikobigcircle.png | Bin 0 -> 3079 bytes .../old-skin/taikobigcircleoverlay-0.png | Bin 0 -> 17018 bytes .../old-skin/taikobigcircleoverlay-1.png | Bin 0 -> 18837 bytes .../Resources/old-skin/taikohitcircle.png | Bin 0 -> 6028 bytes .../old-skin/taikohitcircleoverlay-0.png | Bin 0 -> 20284 bytes .../old-skin/taikohitcircleoverlay-1.png | Bin 0 -> 20333 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-0.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-1.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircle.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-1.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png new file mode 100644 index 0000000000000000000000000000000000000000..63504dd52db36e9da6b3b40a08a9d3ec6e1e34f9 GIT binary patch literal 3079 zcmZXWX*d*$7sh8F#x}MrSw<*Zj9r#76GG8dcEU)uByRS?jG3Y&L>Z(@xi?F;Yl&ja z$R#3%8f(VL&8`_ClNkT{_J5xH;XUv3p65L0!#Q7mcU;ceiy-6?002P5(ZTloPbdD8 z0PJV&#sm)hG)O4U83zE=_nAU7mw#r#Xb0~Y06?_+mw<1gDlh$P%Ej7w#kxj?#3lq@ z3kJ9ZhF^`9SLN^_4ac+tJ>yMLag6+gJaNoA|QMXqG8nw!R z(&_@2bKlWXm3>E0r=%Vb8#V4`9yxhQuowB^pN6Avm1R*_Tkj(fC>SPy>I(j-q9L4_ zS&2>!SbB6pDT-yaL&fZDov1MX{#oQ>%#UwX)pd1s)ph`RpjRCRdp|3pv=&*(w^=0n*?J=65!qZVDI8yKzyA&}eeYPQ_ce4qO3l>9?S36G=;X;;(ak zKdEWCZ0iDsI(O2B$-ZfZKM3C5ezi@Z`3+U-od{7-6iinJ5jG*ZDGj+(czap+hK!l$ z1t=fjHvN}TOZPlGBw8X`^SUvkP#7oci9}7|dk1)aL@mA&H&#=jG+1GnTXg2W5AKEv zGixxfKSv_v}BmTe>*osjH6v}N78ht9`hW$(3Xvdycd2{OEd={8Jv zqF?m~fr_-p#P$s7=VvpAtSKQ>!}%8MT~_3{wFRBGTwivmU2!Rj(OQAoTt;UTd z%k9Zoj^h@@XrmJR`Q)<5d5;&RQ*;@4J>&;@6({o{#eFhvde<--G9gg&A<5l(kJO2t zfGlP2ySrD%abMaeR^+Go4}Q@TtV_ZkyQ5cgi^F=!aUT9`N>u96WH{4s7dvtCGp&n& z0L!UOsc1(YkkMV`$Ed~UpBEW}wu8V%U+$Mv3qe;?W(1c3tLaF!1#Kn=ZVmHm{ko_< zK`R57XPakFQEeOs$2T@`c!rvfF=7d46x!u?-oh9N9%uW)+v;Y#*QxVWDu_Mva8kT3 zT$sgc6Yn@ou8ny{w@H4FZW7d6-;g)(+WFqQP7^7<%^WWJ1WnyImgQJKceytDkiLsm zvUyUcfD<*-#2R6irt2hGeI-YQZ1l8Z?rDZziREU{9=CP7s0873?RZQmTOMk(JJ`u2 z-YKAfaz%CN7+cVQ3xE-w~g=z4qSPzI-_^(@e7BdZ*X~u36X}+CziB+Q`D-}dvoA(1A;js~w5Rjx< z%dYAxPwRO&4gbfSC;cR&1lN}n!CPJzvOf5?(3+=CnYP0-Pc>tU_jjwD*95wwzJkhd ziAw0A_^&>wcl?CS@mj~`5Dl(x>X_?bJ`)uFZ8eu+!K}D66f)7V7GV7MLInLd@84ew zEZ$57+_WsTV2d~RuL+EOp9BA%oY1_(m+*`DJ-cuHG(2zQ%9009{46XPv^oKk2J#|7LBS=Et6D#1f?{3h^OurDaOPW;l+Fb>>7O1~_PJ9$V6L8dSSa1Klc2-j4kCVm z)ad0#9m9sgh!|m=L512Yh(dI=ee}C4N;vZvA>Qx!ms?@Mu5`cRq~eIrQh<856ENFd z&&fgMtRcw#WPKo0kf(DS8EpY!XMF@Um?)-8Rw@oAJSeCht_D6^Vrbx-<6YJtm5{6aeZl9Imy4kfu}sllQO*K-MSmc4E8$@tcpwVXS@bcKEBr ze0vKopOgnJW+W$=m6T2uCOffF1+j$q_QAu$v!M%h*Xl+WuDMaOtlhtLf8~TbN%Z%|1uLzy8RXn(M~VsMU1opIP=B~aB5rK5)QD&Syo>Ao zo7->rVnwgGK2W^hcm+vJQ(2-~|N7YHi{C)se-mhb28aNAJA{L~9WXZb zv6=-cJL6%0a%59J{naG%AQ!NNY&2o-2alGGmw#n^!JuZ7h|77liv_oKFzrpao9P32 zrc9b$`|7y^8y|Wf`w6?hkwqIXAK5j%5Ky|SECE=BP({2NEq&yq5(@*@U|)R?40XU? zU^>!41<`7^jr0JzJuD37<i zj}AMmkqfl(D1|^5MfH{~Je4!M%5wgWQP6SO%Ol_Y)?$?T{CYx{^2q^ zKW^9Up%N^llPIU@A13=dDfuM3@w#U8a6rLIX`m;*gl?loxn-5>3`#xw8w-}lY4c=y zTTvDjG|-kw_9hZrt>r!8Go~Qlbf)!b4I>w#Z#NlRa;&7CdtG*Xnm+hiD`uv&#~LsH z^3<9RuM>$;c9im;~)@lDc)S6M!L>zDY=QU-da z9uJeIJB6w>fYK&~#&P-L&t$$bwinh(e~2 z!D)hj%^9RVK^6LkDjpr*yQ)tpMz&crO!XyT-INJ@(NnDh833^5y8ovAarKTeU)y;Y z3;RX!pOj8AeVYX+_Uxz;n`&Vm?pd3}EC#<*osdeF_Zs^%>I`X|V-B=@H-A$@yUkE} z!DGK}MYqbTzI-I^V+JZuUm|DXk@eq2l25d#=HC?{=*+O~`=+3s&h-3tSqFm@7PzEf za#Ao%DDDPT+5VzR`h8og439Q*c;XLt&*ByS5qxdWEjrQrz>Tkw<}gKf3bMk()t-i{ zy{vaXjS@CU-eY>L#`6gR)&#Ajj6{TV^Z@%l=x+Axg&6+Hc;dVMgs>xzHu*>P@ z>(Cz_?5r!-b0>YL>Zcp@| z?W4e^ghW~Z(1sjs3eY~kX>Zf5CXZpH5F07NBxUCk^U ztURISRyKCdVl-!MT{KWTOEDTfJ{3+CS7|F-I|YAtD=mLjZ3}+~3n5Dy32~^XuP_3E zla;3#)Yr+;*+bY@jOJf>g%Rz4E_2X8{{`adAVwqk4?(EDiaJ!<#oY?Z$Ij1Y!Nn^G z6%binbGNh>)|8R^H!Z|3F&bM> zPgh|M4j&&Mb{`&g7k3*DE+HWV4sH%^ZZ-r2n}?sXreW00}(uyD6?^|W(w zhW>-m%-qGxQ;Y_|>3@mfp!rrwl1D79=0z3H>m$}`+p&TP_2r}e`x$~d2w?34+#%XS#N|G z|5nKV7TQDG&((@U)5^oe%iY3C)*Hbl?LX4E3QN0NnR&XnYrD8O{`Wws|2NA}ZiLj( zS1M)}cFz9@!Sp|Mv63f-r(Rp8xr}qO`QSyNk7*BjSdKro0qXQC6CtONgJJjhmh8U+k)=2rD{! zc$zs|SSiYg(IC{rZf9qSutouM0c!;3)`C1jY`i=?0&IfZW?XDS*8GCp-27(z{DKz$ z-CxGV!s{O+`0xIf|3CYyx!WNop_$|VF`j>>=0DaWtYGJX7+1f4PaZ8Rw|}=B?V$fM z7-2Jue`D&)WJ{ZAA8zrhh+G)p&Knz7u zMpE1N=Px5vQ|+jY+_=ReLZ~m-|ovwD=j{n&k72@$2mn5;6jd(Vy z=aV7_a{9Y2J+k#mhG7vmLMcbfD|I+NLK;mNAcdt*Qibo5Fj=#ZT5h*zHlHPf(#%v{>d! zP@z+Vn-(e_8EFd?CP10+uSR)arlasVm9|<_E^&&9(AXfYy+e-uSD6k=sHeSbl^i=c zO7ME=hu0sON(%RTrFjEhm=|?M&pHIpFj*1La=S5v7*`7tljHff>v{H3=O|r#(WLQz zTbZ<4q%-u{HZ8`pTB%^BY{!Tf-A-QcbB!wfE&5aiZuUeF1$uGhzCs7(`|e})9CX?6 zUayL@SHhy;ep%VVb9J$!tjzG_fd2f{Vq@3!=vTtrFR#zCLglc@W2Wzm3+syH3O_Wh zgbpN<#-F~a$Ngz9oXKZFMkeA3YP?_Ml0UAuiMkXu{j*k_F_CQ6z1? zdz2e6aQ{SBDkz!Yrw$|PcGrOBGk!$A?VHXAp#L_&R*wqf#O+s&k_ZX|DD?E)&z<{m ziJE{kU(Ua>Qj4_J-u6-|c>&p&k%LQo0O0u>EhxNG@Xt`aj8df*srnG(kuR?Yhb>Bg z*^xJqEl44JWdX!1)mi~7Egd+gJIA|#XYaMEM|O%prXh!}o3+M?cSBr|`<~-*ia=MK z=LG$ilUPG|QG8fY$eE!@{f;w4TNwGs;*f>Iy~tN-Trj{oKd7DvIQv7eL(VTxmubD` zqtcfOx}G_EJ5C4^xFAWhKq0{UbMNUvyele4)_G{FR}UUF8D@NZBsLkGF@Yl9W+Eek zc-;@xxXzKuxXqt%t=1W9t=1bW$Z?m}_wey{3;A+?lT?dwyN)(;8+iE6Rnw7ozE{ZK z^9UB)M!~DcNEMDG>{Dn^WGp=-qfd`lXDXuI?PY|iF|v+6Q(hWrul*IlVmN@BZ2j&a zO2a??h;X_sKV65VUs64SY^+yZET0wt2he zhZ=l2r+0U9I3p+_fRmo~aVsXq{jH8pbY*3wo0F51n7zGyZI5YpT3T9jd;3e1Hg7B& zYong79KjHrCmV^izekm@)WP$dOPlSWcPWRQTMOfnxyw|%kzihIQu`{5uZ&cd8R#oZ!c6uI#L=SO-|MbqX2}W|&{@pupb^L2N@dT}NKB5Gdi@LyN~< zY#8dr{DN`6&uCU`%9e$jPZ#zg>X&Ft;`{a7(qDl3Qvgn=5w|Q?KYB5Zig^;B8!L{+oC7BC<0=wb2 z$>jKibwUgbnpYC9;j_gueo9ggGaHR*%rYS_ouF{g+xq;|9Jj>rAVD@hw62FO3_%u_ zOBY7Zbjz~xfv$(&JjNH@kAiP22gFRHaBojgiW^%#S?Y&v_EhK$jhEgkeiE_2t4(<3 z0cQx@^RL^bFsjbJvi#hQYBQv-$m(aV|ACSUJYXc}PZkugOx|2yoLX_4bwLdQHR`k8 z^}fg0uo8{2=)KwyyED^B{GzW>vm3h6i8MNlR!4RHS|yqx^l9*g>Ce<*+u3tNUZiPF z6I~@)BHU)!fKOJ%?RT-B1|>C>fts$r-0W;@1!9MJzoB#t1KZLYK@oO3vsQ#ic(dNs zj95oKk;k(w`9Pxhm*OkWdGhc#JntCQNF;dKxwlZ&-(RETLI&i6Nf>Fb4BCUn?6Xr+ zE*T-lwpRw{8~U4SQxMjXt-Rm|yonqkq}Tr5gGM3^NLaWs%b_Ik`J{`*cg?@H<>F_b zEg+m`CkO2KgYib_0&mb!KBx=au6^%9Iz?lCZakd1n2>m0IocM%qvlwADjMTR!aqMp zbqSY1-|ToR7!DF2=?9sB^1gxEyFu~^MeAV_!#;*7XtN~`ri0koYfS>DD^tX%Vf;`! zsMgdZQ@Ak~o_giZQI594a*HZm?A%74f{9=b;I6ELc;g@TmHp$w}z_F)h zDW4cG0ORYIO`$yk$TNnqX_Mr70Qy=MQoIBOop8i{3lO@&Yl;R_*!JUB*=WX0x+3Vt z4B02b0Nn;d?{0lZgR$CMq{xwiM) z?5isxTH$wSU00C@sGAs*=lP2Aa%=m*UMz;OH{))`hJY=}ypHFV-~D4F=j^OI+D#)B+{Z~r zmTZemLNtmPp&;{>jHy*;GSG>CaOADijEjn@+OJ2JPuwKK=&VamAbxmZiU%+R$1ryf%@J4# z0+SV_w&Jltd&Y0qzjJN+{|q^hBRo*=b0FghT$G0aWTio(HwV(!6S;+R5Z8^%cg7rD zcTzNOcj$ZdbEIOTH|66x>^MuUY;5=puVl7)s>*T0?<*Qk>;<)QV|{VARHOm;ZzJo_ zxf&v-Tq#W8++}vd8MVRd;@KqRNEE{p&ab2MjYPI24=%b^9F0to;&?Pj=>&=p6hGTjG?Vd0piVmGI!scb7C-J(GAQu7o@@{ zc+-t2t(Yg|=Fx5SanDs00a{pFtaCmF5q?^dJ`2-8eZTxK_+d;s0XB=kudiD)e=p_B3BI)(~0sRUnL>lq)_dJNtl3=azA z3p8~mGgWClVnqnhUL-c%}`ZnKuY zzEPA+q{@4|$A9@{W@7{H?8)+y3%y^gEh|sI%lqePPFuT66yWS^aT8D6_xW8t%}nR6B!5> z>-;Gf*a;*|`ynpfvSVBN6y( zHwpNayZV(>?7lug5O-rP-Q)&>a8os_sc05WDfshhSDuIA0#$|!Rk;&i=SM3liRE>z zp0VS#SDC?gBK^yLQt+?qU9kD~D%JKDe)Nc{{Q@WZ;~Z0gM2G%`^C7b^H@&W#o26bT zE5^~Vczxhu_@|lA)M^_rfN3f@YvIHQm9Hs!vmfiHHF`z{NkT2ggh3RXy_;iy@r{`N zw>)*+XT01yxgm1g%?vOi-y-AefY{-D@DfAR~!jE|3n89~XBCUy`Bx?r3x6>5z?~m2i#a+GxUi2c{PUZMCha&+`P23X=Svzix?aJi5 zVpxr)>jt}M6JjyHcRk-^4T@1YbK(p`s#gAJ3gw&TC%2NUZDEDsnxptWgIg>F74R zYw+ruj7y*YV)U!QewTKL@Fk?y_kt!}9|R^5alf0fM8(l~-1(I4d!Mx|`a&G_d0*e5 zd``U`Uc)45J#)S7WdSmEVEQ3+Pw{teNSxP8pQbfr1A46x`c9b!3X*7Q$P z0bZQCL^Jyi?}?|RFC&YB{wBO)(~WyEXEX_OvMII?zTD?6V}$MU+2WiVM|G%wjEw6F zS{_UG3M!t{cT2jRlomxJ%cvp+!`XD3TKJVe|FKpW$p2y?9}lyT!}dW(1u2sW{6rhb zkG7C2D90aYh)sYzHn9rAbKxf|@!@rmj@!Ro?e#X;2J}8vNCjIQ64GTbukuee6jmNg z^{1%Id8+YVek9?2r4c<{RAQ@XKP17xg+ve2VcPnEftwPH&GkMVPsR9&d9ZTDG+3e^H$^np8GH<-pb-Z zsD73lD2cIJy;oXu%%wdK>|}IOHm8il8P0d$Y+Q_iRq6dugE11RgeQoaom{&@Vam!^ zUQsb|uqI6MX?lL<2f!c6-`~aByapaMlih-hb8S5CMbrB~{fHp@rb??dE@b(mWAX&0 zg0@>iNCiU++|ecYjq+nMh@-TE6;l~FJ)@jkfBiY{q{IO%gQ3o$^3t-!>b#q9hCxj# zWgrnL&IgVJ+}bv;k$&rlsidNqTdK$OBlSk-75D0ER<#Ll8+7Xd&5q=9_IfmlGmxyI zG39=3H>Wx(i=>+~YAGE@Zc`?<|5vS?I)jVRa;<}6TrA}@@YX?8?RFR{c4|B&S9YJJ zV0Q?hydyw_Mu@cJTJd_pL7QqM49PDUGX#<+bCQ~t=HK`!%%;<_x}m44F775!>xn*w z-%KQu>2krJgeQ@9J1y_S?(783>z*Zzmlmx@!^)d=Me>|=b35LKFrt^XF$tJmE<+n3 zuPjW3^Ml0@183O{Ma87Z2>E-hNI_YcOuRT<3ZBtNxcaW$59e=QJJpqJj4F6s&|9*L zVhA7!fCc6mY!^6<|MUY(*E=u2JpLNYjVtiQV05i&Yi!aV^&PJwZFU&+z7Y(AQFHU| z*c#X?nHsEXPnREW!r;Ys##p$CnlJF*(OkDKq;5eIzFdXuu5&a9Ts?8$R+wzO@$g$a zj!3zGhxxe+2A;FQ(WpmT5?f4mM4f%`Ev9g3E)DS(JCqzn6ZhZY^tI19Oj2IEW%!X+nN1K_=4pkXzjB{r$R*!=CXA7=9IP_~t7{g#TOQ3qeBFBQUrbPj~mWzRaK8t z!E_2r3i$}zbFXRkzfz4u1%%yL$UmfGFXtmAuD@%Yls7g!0{l%7f0DWj6{pblv1K`xfxyC?@As#iY|Fhu3>COe&i@2RdZe38FNRH!96`-)oLN`m4Wcy*5V zRUUxqbEdqH42S6T45=x{MaxHXxg~(3&~b{QuyJlNG=3xkfVRv!$?*;&1B@XWL<=FA zbT>aO?~+)?kVg0x@D|*0zc4xB+k5C?SB@m9&p^&8tx1tH4Bk6OhO~={+Ardg^mf=* zp)7jxnNk0g-zuU}f9dIs3X+L<{}Y#ttUN8+0EC^w>f5&5d|&obzF(4 z$4irsiDc4c%u>iU!Or;huN{FFgrRP8cmwDzYj6Ju*Ng1OAX%1YSZX<-@@Fe4e~bx3 zu2yQ4V3tZm1XUBjkQuvIQhZ0q`RvR?joa93#=b+{p4mBRyW_V6g1I6#%ws2ZOcS!$ zuSSpsO50U3*ycc{${usF3qZp-QfR6oUR@HersU;UgNx>OZ90YbQd z714u7YK6)qC%q5FL!8!0^<{~oSJ1J-!w4X&iO~gtVdj2+H?DeOvS@ha>koAsG51+) z0V1_38>-z&kIAy|EtFSM$|`SgM+_1$oM|xFjoaa-KEMC3p7 zz6(oKRnB->89-xDc_&wsbaQX=;xxPuVlHsY!Aszxj2ri0YqY#Vz%o=^a1FN;jIvgQcgl4qC6=Nm!o%D(lW z`v#F)9J%0Q+3l7h^qKxM4e4_sM)iw}uvdy(o~UY(ZJByWJrxpWv0a{&p57lxh_IVeLIoXY$J_`vn6sBXX}YVU*1nX-p^CY@T>WzI5jxd8 z=YMS;0H{FH^F$_my1~j65quFJh2e*+2#w!Ex!P@0wJcX<2`o8A_(Dk1sMk93zcWwKnjI{N$Oy^?TLk$Z1YsnZh8a7^ANZX zP^##@WSC0kg~s)dr{T&C{jP!au?bPN@3WjspfkX0z~0ikQ*ZQa2_K@*^o-MFX!eG3 zmn*nwfd#a{`Xgw(R3mBz&5#U`m-dz?OThfw@O7zLu2qFlE@|(Z_%dvejU+wt(m`WS zovm*+K|BbwQjk`-ofo|K&0&RYhVF%1gA`5SRNqY@M!aNWUVnY*`F?1AmekpXGy4>J zFBRyOyL;X>G)IypMO#9G+0JpME|#u$3AhHDaOrLwwN(*UQi{2_o1(4F<7f_Zw}`qt5uXgKKOZ{chzzbOwnL*B{IO_}}9XMjozUS34q#T&l%@;o3vri5FLvU1i6fVbD zK6xNRuY`6KTJeQFjoxk+)e`F2;L#%>g9dGBfZ%eVxuVl-5C}8_djgOs>m2yfZ(qM& z>UPEbto5tkS5bX?ow?nAj%Z0)VG_W~F-ovfg+)?PaV{21JZ1mtCfo?KJY)w3sMh4d z%y0|A%_rA?#`DK|+FR6PpUJ!MyvY;k{+11a9|6KZt(htDg|bg&E*Wv5+1oQ*NOG?% zH7ln(&X3hGG7!rk1%R1|bPOuk77cBqbr()cx%OG?6A?)-clqw_n~o@=bMIDhXl*=~ zcDG|o+hT}~fJj38O0hlV8t85v0+i|(%b{Ufq@oXiTi^V( zq7#&!d$=!_?qTEdH;4FmiDRC}FQx1~vN27qX&)%67k6 z71xt7^-F2@Pf9)>CKoP`Ni4P+Z&a00E4?j*)ayi>-j)C`)5TU#e-forhg z1x3*fF{(^Smvz!kE6WU-XZ(h*7@qYp7eQt@R6@41v;0=r;_)<&QXfFfr zYd)w^lvVlqAqf+R(EG5#p+9Pdxb+Gpa0Sut$cuCRdjNpAp3YhyQ>Yjl4GqD_E6%3@3*Tt{v6N!u&AaFoEGOY z*!7vG7155L(9v|Avqn9avT$^3(z;w(`XCa~>q_7K!3TKz$0W zCXoZ+3>5GI+vzFM!sFAFp7yYCe~=%`Dls)oDcsv|@6YAdz&c zMKp}v`oQCOeqGLw!!o|^jt42@*5hQf*x)r>nY(caY*KB7u%rd>rz4_fICCJo`FyTg z3J|UGjg1nwW2fY^fLEup3%miPjvm`mnrTg;%1m2w6;*L2 z15?>Sf<7nq)wdD@2f$rNX#3k@;szgrfv(fJW1ZjMlw6)lH_H!%}i=wTYv zltPZUry#O!CLrvwDG4AY!7c4Yu^|x6vUtEC%&r{$mOWj;x4~$Yuc=_@Rz?#~f>Au~ z?#I&@JuK(k2+y>(IC8Kyujn!-dQ%Pcb$_6dn)3(Uu=`dP`oN_rHHlNvNi<+{sK-#I z&rh`Rs*7*{ZA2>+jz~4jAI~RI=XY>ayYB{3M@kiw9fD=)EY>(Ce8H*_v<_jP`eXfbTT=mKFRb!zuZ`nBf%Z zcJ;NxVJkH(8hRcEEaNIWPEY^!_7%q})s%eA9}VV@uJw@m<$;bdU)+vCQ(xHDnIt1I)3g!HySh4rSrY!C6lWX77?9tRCyw8Zv>~+>|IA9h9H?z;$ zCyn_lH;S`%_ji0OCh&<}F)Q`n$UG>Utt`zcW^|_+Wehv%= z-Yne!DF0iwKl)7gSp<`nb1%{@#MKgxEA`&#-c46T&Es~8fN+~Qm0rTRVcpWA^njkG z35M$_JM1Vra~_DE?T=~3?_ggUSCbc7a%IjqPmVluwlh{nIrxAGorL{t>5V*Z0UX!A zvrO5z_|a{D@IaGQ6JUSeeUrhbcF zS`|6O*l`_V#j)tbJ9;Ccz8Z6eXI09u5XuG=k#!LcI%$p&A?- zx8yXi)K>sIdK#thQb^KN2AJelAwIrSK$rGjgO<4Go#L4Lr*LGPsB|USb5<^g-51jJ za(~A{uQHH}DTZ!Q%EPkg0D4BqoPwwmou|%_yNk#(H|??nq9$))yyE1TJiw+2?Fsas z5R)KV!KpPvMsceV?5nE)vH-ZYh|~3Y=Ym$Qn2tm3b)3Pv1XGZcL5=ZoBz>g#SG&M1 zU+&1sWIp&j9Srkj-iwVhZCXKUIMzT7ePZHkN*)F-ORossI$9%YAwkX0cjt}91(hb* z!k%;kkw<4NMwGL3z_N3POc{~fx)V2Iy5GCRsCBCJpAX+k@~(P_Iy>X*WvH&@fu*W?hpf*at&n0h4Kpm4ogC8 z)sbj$L2;UG9iLU<&f}u#cwbN1(YncEj3fsPl}*~n07Io~iQ+5ot%`YFR?XG7sR0j* zO!?k5NjSqY>QZlcpMEu&tYQI+Uu_3uL+?B+UWY4we^(91R3t_d9*3^J9G zVZA+yYbGIeGDzxLw)i{obg^aLSS-Pe9L;OZCT}!C_Q!IgMf_;9Fwjx>hyF?=@WXXi z2_r%T_%!xho(+PUfr6@V{m4^s_hA&C!I~WyE_-=8uEfc=gcO}H3 z;>NuXadTP6)&-$#%E^Pu(kSYc{-AS$u`FAXWRh37f)lb@0Lu~hYqt7h>4X7GG-26A zh?SW#4cd3xnT=vjY$^|VIR0R8!F7bN&v{)x>JPd%{_*y5wB)cgY<(`YMGfWOKRmB4 zdJo@eo%dtxuUfsQwlkH$!%Q;*OB1?B@)z*W0O&-5vjAakG_Rg37voWD1NK!BQ8!he zfD~$Dmh^1&xe6_^YUEcPQhXJ-rh~HXxQaqb6&f!=&r@Eug6~q%FtL%sz%B0^#yA~` z6wtC608h`NlCBi^rrcQ<_~i4O*v3P;iTVtCeCU6SJLK?B1o6wXilR6>nsk{&lvzR+ z7r>29@6vReym_d)-Q_N28XHmkLJ{GG1EBUt!S@$gVa9^k00Ceag*m=#^Vk> z6&O84^EVypmHL0+HdKUdC*((Q5+F~Or%hDQ(eO-x=tPD>m-n~a^|X5V?^Cu%8_sGY zxTzXjK`mjX)whh8N5ehQB-Whdup0c6&)onjq42Td=uFcdJ#V z%O@&ReQuXZ1xuunx_ZT}EuqRrn7+7$6zJ++qu*q;F(d^ZeDj4{mN@leyVg*$&rkZ% zmd`)ICyEKs#uMRL$Kd{D&pR0LmkLJ)`MItQ_lIXh#g(UVq%r_+4Ettvc!<*ox(G-h zmFf)vfTuRi&^ceQCj&HI$WE_L;5-MM84KV`@ru11`MA4k+`vP5Yg~IR38{^@xNkRyHvZQkE>S+ z=Bib+1d={#PsZ;>N|ASb*z-Dutangc}Lc@vNj&CW?B3!mfc=K@$@VsqeYNNBBar0GN`*eEA=NKj;?3Q!6Hp>*T*Kf=2Z|tA zPhQ{X z-JR>YRBLLrb4Ij0h$>13`Xag@8uWumm6$f7K!~lN=UabnPgtM}Nq!qo{J9N!UTV4n z=156Or{k~=pUX1nw6&0AX!B%=2kI-tQ1b2+xCot!3ZVTn-ppW{mJ+46x!yf6-WRjB!iAUb+$A)@q z7%sL3lWGYAV|g>3PAV?r6tm@eN+?pJ7--G6|F;!;M~K7#RpYF- zrxiFQU}{0%7x;;#hQ>oSD8*J5raz63QLfXzHXIz}(V65kcX_6?oEa@XEy3k7X* ziTxoPWxLH(ZMqhNbBg_7{0pW?+myoWBWw3Wd<1eK+uvC%! zhUHs+)-{ZT??*;PT;=>FR=$0sqPUj~yPVqmOMx_f|M&K60hJGtaYMm538sj!#=MGw zOg&QJ!mnry*!hK7!6t&i;~mlv6re+Shbi-qmuBLZTBtq@}q4df1bWRSQfMk{|%KJc#j>t0U#IwfcCDe3GZ0~dB4($xbG#1!1r%*ARGbdFc0 z!`)-ws@MItfwAMc+R0_|>Pac!yAHGLos>V;2 zU#4!E+AAmZY83l|ZxnD$LkO!x7Bk)5WN*&O&UYlWJ&)AI(1RtSes~1SXPe5!q0Y%< zLE%HWLDgmxjN5;t0_^z{jhf{Ky}&CNoV<5N$pCZFDT3-Re_wwsSTL!(4)k?uMKPZ4 zU+0UftDy2M{gzwrqd3f$y~gj#1q$xkZ&Ay~&KiE%xG9sUBNz9en zzj2h>7hj7TKQT!B9rQplK0(vi8fV#Riw1l_h6^t{BqldJ8Pi#cj|eUIUM@s1imy;t z&G_*6>T+g5V|TdbgAk5rY5Qf=AMap*2U~D<1FH+I^(O;024k5xM}Z_RtH`+ zc{c%r~03iYA+<#u51=MSRBAzHql~8ASPKq1Y5ug*PepC;9&Fu0Kt%Asw|m zt$rx-fKsK&IRkz?-5zTiHn^6AC5!eIM_W=<8b>4fdx4W0foZ6vupg%>2#N@i$-Q@B z8uOy-E_X?yf7XOwOdsMo+HJ1J^?QD}Rlh!4{YE|#3aTxObQ(@?S5=G)#<$$#^S-j3 zB|zxQy$_O3!skxc>h-%%D7pKzk$|l**E3}^#tH}i(Ok4$d-PoNS=grHVxKt{cAyhw z!3Kj@kW+W(HQLVDQV%bPj~sXub&Z6LZNuOb>oW5v7)DDK(^hc>f$T0^k5JP%F^xwd zp(S5{%HuS)gC$KB3 zhGo7Ae)`|h%rYUKY2Yc)GzXxYq_m@E~=8b zBLVPnYvpurB~~jA`4c|u9Cd~8z2n^?wz$NnEb4FdjV~vR&No6111Jsul&xnD>K4u= ziX9=y`r0lbIoFvVJ|5)!TAoVo#~@B#b6K`x?iG6w*|1At*kG1U`z#I2VcHoj4R(tw z|Hc9$LFaaPGIrq?qz7dAHS!o5;hFgVUG|JExJqKYe7N3RTYo42y(1uWPAy#R2=v}w zM`%Lqn~?il5l)d#B7o8h3sF&Yap!-#Sc@%od1YPiH>36Rcz@+LL*(F`1*K^?q%__; z1bx%oCwLBffPSn$ekZ8#Hoxm3x~=TRZl!Nj{Yq-P+naV`L6LJzio43{=dCTCf+(3W#YxSTf~myHRsJ^(`0dW z-7n3J@-p!0(`Wj`ns=B^fSzO@bI>QfYtF?+G(ENa)9J_;hzr=kt3Dq zf(d6II=?hhwe{rD*uF+6q@4c zIV7FGEW*Pi49dlYTTWT~C6^8;yPpzR^S*E_pnNw_i1vqr2c|f1<|)$Yc`4 zhoci;B}Y(|+j(?ItWTgpzEE9)PwI33nd5qb;lsv~L5Nk*j?j&Fk3ErvmKVU#i!EOW zyusEmUXz z-}KhVqWdj;@I`%_`4ZAd`kGJT`Zq)V!Y)E!sY))Kh&_WNhtqpjUgq4g zFe;Ua`h=c{g-Lp%g-!aH`2a2u;u-|PawUcs^^;e$gDdBjQ~l9B- z7e~$r8i_1qouR(=RwSw^WB-!%8+O&~Dy-e4r^j zwxC~W>4+_IUa?HD=cSLTLlOyB;vqI1Am9xUm7R4>GG4LG>JVOMUucKpQ1i{~dHp@D6zyY7n zR$~^u(ifK#qVY9GEZ*espP@^#KUI(i)o)6rR*s(&$rW4q4i!s1C$h!gpS5}b7v;nb zt+CBFruYas{}ODOmikCNTza@<(NhO-eOwLZXe~!`-T!qtSt#Nx|2-1>1D3+^eU;`* zM{2{5!&q8+A8e6GZX>KPd-DR@Q4P&R9gS!n#3*IXW#1$F^7u--;h8lXe)bk5neF`X zg|NWmd{g*3t&UcHbo?N1&Fp5B^ngkhj`uyHH;Hr3=f~YeN@1^*s$v+Dz9wg=KU)3` zNci52^sa-aRJB(=;pG5B$|02`IJu&veur%B=BGomiM>l}vOJL|!HeaN${~JdjeH@# zmW5oQrm?t8aW8bTMGmY-S~O0lAC`!keWdBXuO!DhK%+IfF-XVX8-^`W>u{o&!_T(A zN8-%=}{zW!uBPyqT4T*K)a*I*ga@1%@cY zm4qxj4*00?e8o#ZS$GUU*?QsXm*4=0c)Exr!HfCmhnW>a(Irg6weYvkTlp%_5z9f> zAZK>A*r)Y*OkR{^tJ;DJ^gIb4?6ai~a*pT@!*FG4^b_BAPhGpK^d~?EwL)OTQCV45Lpk# z;plKdQ&o&A>BxCYJ3J~0siF@}2QTY%Tp4JWIh#l+&(eOv_EUCl9%dG(0-~`s37}#o3z8J8@3N4OD`68!hx3my1!NIK`c+gpRZSV ze4jhtc}8-(F)9ACFjq*f%fKxX^1!mppdKJ+YJ~m%l_K>MnKL(qX0%QG4j|F#OpzHhIV;~^ZVqfIKBFiXfxT0?1mzQL$7Fb(F9Qn z{3aq}zaHIFx%<`BxH%!d6q`sP6q@+_fFX_p9!3fXP1T}DCPYK;5gYl@9;%ij93A{{ z5nx~usXXoY?pnt{#$&CD^zLIEuvyO&93Pnv4);htV)kk)cqLiV7#PSopKeDmokDHj zKTyW`2%EI;{9smq*dFn(-EQ52g>RIu$oSgln=o+Z=nwdQl#rENy(*g*wLQ1Ts>6rT zF4}KIb*G(38nI5}D_Zkt-bXoR(Num=s-=e&O};o9cH`1^SV`u>O(e;bWUr);%ci^&?~ z#^*ZrDw)nYJiw6_hJl9r1*c%}U>1Ek>FGm3!LV@JZw>Wtc}RHd$skBR#V8$bOmdGd zLG%}qBsT4y4gU`%I_A+S_2rgqp#v&my z!!WX{^fy4-XM%UBfz}Y)+b(IpUkl&uBZ2c5mgV5O46qEQM%=-;AWx=E5p+ z3esK+I9Mgq+JD*+#ibzzYoXOvkY8yA^pE<#)f-D`U*Bj(u>@IYJC@3PBV0lWd3|fU z^EJ&iDA`vCGAlvoEtBzRunalZ!-YvgHju%#pLK?W-&dsB*OErS(TyQz9oUwFC{k@O z`<)(KNpQNI8uk+1QG&)YFtiKQ1L6(^4XgkO-2laGgk zn~w#;#lb1a&LP0g$<4~aDa64i#LEx)_Ye9i&C}XeNJ~cU-?CosM4|TH-tI!|?0$ZJ zY<}EqZk~4RoPvU{Jh<4oxL98)SiJ&Vy)FD%UA?IPn}dvvmzAf3ySIazE94)J7M5;4 z-lEV~P5;XT7x(|Lb@lqUn_eBp?r-7F&dJ8{k4gU`w6^*Wox6{x^S^{!Td~_X+ql@c zdV9Una{h9{f~_Qtu8Ju{}JKkE$jPg#=jl%zoqul z32?Vz*Rt_)^YOH@k@bC5llmWP+=Zk)Z7jUqJaydMod3I_H2zy<2-mCC5C&BXD+kwq zj9~nqs@TX_c-x3VU)zn7mE*P1_;fgUggCi{c=?z)1cf*_{)<%A&Dy~>;J-<^I9NG& zSUEX$IC+G4IfQsP{uk2Mps}{_w)lS~wzd+ob@Ozwcs1F<#lp^p-QCp=3i*#Cg{0k_ z-8^3fziP+*KR;KLme%levvqKOE%4Hkmx3tDO7n3F^6{~9v2p%OT~$>fMOQCx3s)-} zMHx}(t9#fS9IS;bE%31-<(od}2GqYS+%aok|Re>QsI7@~8#7;=np1-=B0a ziCnHp%{^Y}pUu-BS}ZoMb~aS3cE==$MYZgXh7 zx8G+Ss0<2XAfMp8nFJry5|#Gzw3!zi@PAwjec<9yD2Sh6+KN0oU93ahhkjCd)H0~{ zwUScVN&p8guR}gP*2%H`&L4M5P2*grLfR~$v+cN7>-=Oo`*5rpsG*i8`s0s1UnFn-G-7hb^TrhW;;>~IDV>WhbnQ&Y1>w-Vi^L8ii-c3`T7TfZ4L}jgk<~hyF54PIa+AZMlF9R2K7%3#b7da&zS(gA}+!n6qQe?Z{Q^Jco#tI+O@P|U>^ALTHWtzS1<%Jq9 zXvh}=1-fR-F@ic${N5nmFBF@QKI~>+3QGi^`N+GePmVN-Cf)i-dV;WBs4erpiT!%c*e&dgc6`sv^wuBbNUK0v05RiGEU!%$@cR+^zNVrqI726B3= zW^&p7N8lf4&q2SB(zt)6zQz4jVj-=Nb4fwNNcV2K2E6kaqwxHD>!FqSMSXB+ zP{GCqp|q@7#?jGH!NkM_-qFcP(Z`3k$$KBCw4?+=$ZkqwIKTUQT{`wx!)ZgJ-%2p< z+rOYRZZDzz8Sb<+FeSUXudCTKnbx$x^t6U*p`0i5dr2Mx@wIHs9!0|YkMV7a0Dfd_ zO5F8NnMC|3LDyk*N)CBSB=(vXk&`h+5nqserLT^=NK7e+B)9@^pjhHRMgvTjN&Kw-3S!{HnK5f0COXJrYN;wfL#WH7UZrf#TkOgq@(&Y(~_- zV-@<)szM9)U~>Kyh_8GcAbfNjnic$e9QgG6pg4!7#qSs@XlSUiHmf{$BIf^*p5Q$K zuM+L=VG+spmAbbqQn<-PxQfB!GTt(jk@r-obvjNHL7Kn&YO6o%+OcrhNOWD|fT5_d5 zm&5tVD{DY)v&7ZgZ!fjE?mnLnvuNxl3ne-pQ2**x5r%vxYTJ^PkEwe5@gw$&6-r`y zYKekkTo6T116Pp%%nBn)!!nLwuAX}T&qQot5na-(SxB_Co!!%R)C!c?(dpM|$0@xs z#m}6meV#(nbmX^Tgf#V{!-cjOBayPjsW~dANuC2|RFMlWR>&My_bx8h7lX~>Ec*wCiaDF)pRNY(;n$(K#FS`peGjn&$Xg*hzTujhUOYv{i(0+sq`af zoszmLomc ze(KJ;QWyGoa&8Vj=%c<)GzMli(QyI$!f*GskKc_OQnz+?o>3wQ(_0CaZY$4|dC`wa%C!1rAl)pNe(fA<1r~<49u#nkkNC)vfE>&i z>WX`Y02a6xy%v5fgIyqViX81xTJ@g8o|4r0e4Bv$&<{=i3?+LR3@>5lnPxh;6eiTCVl zT3ylCeuLibC(QS3ow}!xy?dVvn@Xfbruwk!;s7^blUwK}sRH(QZHKtO#74Zt(LZAS z%DB);(WXo|KM*o;Hg&}ea9@X)Az6!6a|@DBh0sAeMrEor@jj6924A~O(LFpKBf}1c zHs_18(cdDYEHGj0_0t)boY5-43B?O}EZKYBu5~$S6(5N&7``>{=@=oi6C5ITT&bC7 zdG9~Ul~_w6tx>Lk(6#dc1fT(|VF5a@6ac~B!f>xCZ~%z_$oLrJnb*-B#oSGZi;PA- zPF$BZ)FTQ%&h4zV$e)bc6h%LbT>qHT;JbCtz@eh0GYvJ+2fZ|=&3?$L@mq7#XWCr}I6%5qbvHt+-h#eza)vZ3y%beiJ=FWKj zu={JkN`uQdB}*XSwRaQ#Q?VKmi~30AlB*_IXbn%hOD<@%eXVrudbroXZ4-${V&T(ptOw`1PDL@ zydb~Z7)khKGZvwZu+05Jo3P!GQIH$6sY-On=w%k-kbRM342$(g0OR zFArxnr-bcPzC7BXoIU+<6BU2Fc?c=!xoXOH%`&JeR=FO)uJRkx`P;K1I%_^3z>Uhb zXj}5saiHBHpZ?yf%UBy#0dPx+`)a>=1uHyFVeyRfUV`W9uZurQA}6}B+*PfQI1{D# z&fP;|?opvY-~0*c7}reh3(S491OTa+L+#0OW6*$X)Hb5`Uz_Xfg^;jdQO{oze5Rqi zrXr|A-ET$=Gix>kO*w{~`-Msby^x1#1HvTXQeiU@VQ6~QM1zw7+RKC`;6Vc-4}6-r zvvYYGA(b&9>^CJ5Kp_G=PL+AXKwW}YLKHJz7;)6eXWOL`QvT8hKgJ(rZm36VVV4zkF&gu zMi4Kb#n8?O4JRFmaB@P=7aXgW2KuMTpo*}|ygo~%2z+JANZp|?-9H8>!#@pC_>roC z3|h~`8Z8K)Loh%FH!BMz*~_?C2)-Vd!RQwyXT{4xNqG2#rXOO>Vlf&QDSw8^LYr~au4 zN5|Bgst(i~w_Eh=fCEK?Mqk#}my;IPuD1YnNYGN376|{oN132@H~NNMLna2%ZGUGf z+};|eUn?-L(Q=HPk~RsV~lXxcER^O_*JLgCX>>3h({+A|W>^uj3pf`OBB7calbq zv;k;trHf81L&WwaESB9(Nyr-d?s(|Y#jwWddxJ8i)6L=j+K-Zwjg+gwSF|W-lQ>Cc zgbjEfg`#m@D!&!$vE^Le#i%bD;{wG@J62eO|DF$2;~bB8mFqXQg>^08Or5EbjMrOC z$O#2Lq@~d>V;%bo#X+ZyoXS@#N5yHY7>7YtGkvdbBDi)wf*d62P}?vklWldPg$5?A z+r3DiVd6qD0+R^2dY3pk-LwAksv@z6OYIkOI1P-mTj~V#-tod@wdEg*( zc3-cDl|ntK)x(u2rN@eM8Mzkt9IPa?kPNXR8-!HK#vr@HBfARKlj zBC;D|d0syT-1uQ#!=_5iu79ijit59k7vinO!kg?@@+s&X<-GP<&LGnDs>NO1?G?h#+cYgXK|?Zdd1 zzO#(MyCeKWz&)1oe$GW9Z}Wbqp)c%L{*N{CxwHac^XJC^R!bm``E)ha*c85q&efD% z;D*m>g^%(r*>g^<;ULQlZSCEH7DwK=2^eND2)WrBJKL-r;qcIaE{VG_JvJW--~Rou zn|m2dXUacNnYCLTtGi{A1b8_-p1o_bgH3NqE=LdVph#2^j>KPSfzNiD)mmqM5L9LO z4d4-BAaZ%#^E8ybtN7NyE>w}l?PUAXG&gjbElzmS43~+_v3b3(-#aY)t!KJ4rKNm3 zU#ZzVJUcG??7CDtZr<8c{R(%*yp+wvNZ%9DUt0D(h{Kr4V%EE64_$=E3;A-gF}98f zg~m}x(#%9&qY>Vq!5!%Q;0PaF6H2c08Gqvjt?2Tg?F|Z7cJMFmDAQ(uIB~HPW|7dZrX218PB;t~RI9?qi2B=Ab ze02k?a3eiKf)!J?WATMuiv|C<_Tl(U@Vt%7MWlT343WS`ZOzC2j*w=~Dunc>o7>lT z_#N2!kF@I;#q*);#_W9Tv3_BDniTZMrP`sO-{V(JZ$;BucW7tl^AnrAoLMe937>r@ z177f_g_`;Wtey^f{Yv+svxvs@P!9ZU23DH@a|SUqy;cit8_wx9)!7}Jd=eH!FGKJ& zL9TD7sg{;I0Y!%&iM5Px_!}gd?yc}7HPEj(4apyBsEm?adwKd2M6}ZKV;$;xm`)fv z6pI%~z_aHH**UhpT~c#qA*|NlJc#78aDEG4PXfreAdw?ob2oY*>^3D2WZ@crGT*J5 z0=8IOV17XXo3yoz06GOvuC|KxoL-B^kgn2lHe-|(2DP4nRq zkkyI<$N272ius`Uh=dubry8J?E#m!r?0TLdbQ`LR@$8A4L7i&^lXXazxLA7*`J$4} zLD8Q;{DEi_L=V^7*KZs%_tjoORWZ9zpLt04qalOPlCr4!jh-DUU3t?Er;}=u$*s=Z zNN1uSs)7wwYPKi{Ym69!cp?;|+fNqO;bfYfFD)G2;tq=MGc@m_a2pjzv7L^)s@6!+ z(1TIA=Y4G?YHX_2T6U~SEX8%Rh?H`+D&v94M8{I>tu|H#_l{F}tHH^1GX9215i($VKYnrgX@NW_tjTGH{ftjr_IYtezR_QgaupMg zm)=icv-d{g?{yx3E5_#~PGl}i_ze6|E4ZaN#)XEE_R;6&GHhqU+Ah6xFG03^X@fUq zy}qIc{n@xbf@am?r-isuMuLH*g`Xj0mMz5%R0^tJ8xEj>)%+dS@RTDvYUfz1^$FE5$w%&0DjF?;9m(2++ z^z=RoQaCiUG#mn8h@e;la2a)K81H&>)`vloh>ig(zXZ%g15m?_4YTk^^}RT;ldY-?Gc&9!@)^u}8G+p*0KN z#T2+Mudjv~LCk`m8I^svf5}Tkq)skH&3(O8dhn#^sJ0c z$s!$#z(nf&sfOa#rLLxi26w;DFp*|qa0~XlJDrfsE_)a3?>-2KOTLRxV~ns6l`_}r z(D+$&N(p(XcFr2a1ki`Ae60G=sgKW#MjBGdme$41yYz#Af#DZ*>06J&6hGkxcaUm; z$yYIZO-o}unqE0BfdqINnK>{8&|_kvjmJ|FJmM(n^>;krg)($*Q|pNu=x?RF&8%k` zxzjo2asJ5i({IpxufhTA2{%FlXAa}O51~txI^TS!)BPta^$}%SC9f=dOb{JLRC;El z1e;BG#hJiH6t2L1@OlZm9WS|OA0Xwo!t27El88pD5$&S)Ad*vRi*_=uFwMByN2*X< zxG3&k8950ZgT0;-KF8AI8;3*17>pxzB_$=}#ZXfQPW){dgL35wr?x$ivM!3Y+b-4< zNHYu2Uu3U&W~8C;NX4iEUrMy_VUOZ6VRSwt*BQ%CeGdbnBMj{^y2pIv>B)2J2)@ht z=TMDeJpy-U%CfNE1TUP-bUigGp#;=ef?k}HQwS5Mz$yy`o0wn_8=qhY_q*1S6kN4V z84u~K*O=Icc&^4)&6JZiKE1I1Pum5%>AUgon)xykp^ zU-eL)pPyfD3r8Y1@UbT`AmX_7FmqtuH?pV&y3U)pkun+m@?S;7zsBf$kT4{0Pr9xh zbJg`pNxlcsuyK)D*3=4vAyA&)kBN!MS;ol&bRo^=3{-!qgE+q1JDiI7D2%fij^G;> z-2H{KN6jlELRCMBZ5b$%X4iS5#w%*&ctp;Pmj^d!J=>QYdNu(;K{63+Gk> zS64pDq0_Lg`emixch_hGkj3-newy` z2l>ft!q={wl8Be8^n&yAm6qz^n@uWKhMzl{pjeq2*|8QX2wOQzNFL9y?k_!Y{-wA; zSJW3Q^#1Gpk6G5&bEX>JC&=)N=TFj;JedVcQQlPOtmto3em z&06LSJzUh@G!JQGf*#ewhlS8mGyTc>NdEcib*s;_{WIeU6?@*|;iFglkfAox5O3q| zsIR%j`QC$+S4xIeVac!ox)>yUaZ%n&rJOOMWn3n;V{X3}4 zUZW}b5J8bHX4WpOQ>-ZnhA31x6BcT&gqL;1hyErN9RN!sCP%=1xp>Yr=V-NRa{or& z*DFEP?BbG2%L)TVAv4v$>`g5md<#)oS zU&u2AhOSk=q9G9*hd|LIu8jNj3E6G&$g!xibU1~ERWNBrVSJ7|eHWn|-zy6xcM9bA z>F?_R>Vza1{W-x#BWS5e=mOLC37Q(OzTOB?rq6FcbTny9yR4qrg!QLck4KR z^uAnE)G95MW5lmr?){jLrYiX*N7hVzeKkCy9jEzyb6wmawtmEF>noJMF zP$$uSX)#^bio*_&G|_De5fqWX;SmZjF;|3d$pEsH z1d-MK({Xaq0$^}?4l&kj$8bK& z8lqlH6n}qz3B5|uvL7f7--sBn-$a+U!LwHyj_~~*=Z*Yc5^e0+p9WC6^L~V6j65|u zNo%ES{QiwQ@Fw%2NU$XlaEg{W^IpDQT@u8cDR@a56#!SL*unz{8~3rcK2t_bYjaO& z_VK4&4`Kx&C*AVDr%7U`X$jZ6zL)j(ifO*O`K{8mW-sa!C4BRC0w9~&jE1+fD>w{T z#j9b6RLMhNmY79-et4>vIa>X+jaNM@jAJJZmMBPM&o7#>XCx?NLcBg4`G#l0_DuEp zQ^XAVm4v0lPLlLmEC~4%A|H%wwLD(4Jk(kC)a{)}?^*flODDTSa1U(x7IcXN2DQL4 z)=eO$n|}HMVb@oWafOf>OwTRuyR4^fn59-eND0joHcmdG=cMf|IS^D{4(_VzSm^zO za4Xz!QSiMs6B!l}0A$aNFGDiIb9gKPvsg902TPM~ekO!GTDD0ZM60v_>a)`3kL-nTPISG%}38p)W=_B>6f( z*<3ey26h&>2+?f*^o;J5#(6QY(aKlp>!TG}DQ`!uj`9aCIGN~u{(LJ9`5A87&dc8^ zhJtwdB(QR(v^~7^wHh@FQS=p47m2rODx&Xx_?2A=ZM)4DgnteE^llf45#Pm_txiIc zKlU4FoJz5t&ArGg%7OQsXvDZ#?Is44XggbJibL^T10b7d93!J4;enYBe1GhTgWUbn zv>@-lcC$Kv&1v+r+jOoB1&Cu+5Sok6yDuf>2xsrWn_fRD%hf0}b+;;qJk2L4k>In@ zi*n`)56d%!|5cU2W8v5bobhp_IpBz9v@vrvY|2ex#&e zznpss*yS~AldHc8M9FDTuT#6KwV4gPx;>%lOvHhU?CI4o^6yG&k+6TaYg{osEE14R z(0IJSKsWeE8?bJazQ_WC`y1GLfdnK5n0_-HYlFeiO+^-`~4O7ci)42E{ zAEbaXTQMT!qa>9e&dHlM@?vuOZHraR($ z;IiKIL<_6obMIzh<>(AUuEfCGAaDjT32Z>fZwnw30JsGEIRMCajK8LG=ywe^nmSp% zjWcGW!?l#kb`|mTrR~k{B#lJv5QNjXO!khcBk1hkBW?TUMbbDB!`j9MN?hPwd=%3} zGYty$kq>+XPZ;f$Uq>!!$2!DtR$F{sM%QaPDwUPBc7G9b?tpCukAmEFCT#aj9R6S# zsiqL;pexii(rtabJ#rtUS zD`3!7qdE-9Jvz<&xA+RW8jDvfKpLq*0?;Ox_WR(E2JUzzYq{rkAT*908^qRDily;2 z0NvIH-T;ruv6B*Ro-qOKk28*9Z$!h4sGJO*7AGa?_hq?;j_-c_=+0`ltl+gW1^0s) zZ1Vn79F(jhu;Ac8bC{dXt!NLAnIY;dvulb^gag;Z-hP|{FZv(lyeKtf!vhJ4PpjzJ z9`1y$gOuHMkGzx9D)Hm^z603M1?N1zFAIFTLUBg^Gn%WrbWUO2EYjhCmu#Bs? zi+P;x2bW@5;JcT~RXYeq9L)|uVlGrMGDUR+3mk$hd8vdkMzO!Uum5u67+5U`z?pXW zfHI}gCv*A-`2no2FyH*z759 z&kY?X84G7Ljph?-rwZ(*d~-Vvi@_#xu=n%3)uO7MGdfgd8AxtHi=R;DZVuULxMfi410NXLvrW2)7@d zJUzKpfR)qJ=CttVw!`^3GZJmA$d5pk4I-f!cU~{RYzkoF_6bjX)s|O^8&utM7J2!5 z;y1NC9U}Y4IOA+FbJ1c90;6~ftC;nEIW7_=wY7XtND(t!1}%Wx#HPEd4ch8o9#BxO z3SD(um5Z20{k&2<-vr3g3Qf>s8!b+e6tjoBFDkAHY^&UfNCdD$u|z=H7(58eC_3Jc zfTei^5@h!TF>FM}=f|S8^JkOSG;dlpc?;0IebS+ki2sFk`2(;w8;6_<=;38MErril z_DUMk2$*HYk>JpSRWujU{tBo)kNu1eFqwYnv&+E|WF2`UPCPvgGA9c{D_g#}lorBH z#U_=-q!im51dZp%Q+h43`F|-iKVX%WFeMr^TylpiCHZN>6VQWa&(pT87=qKyy>()sB6U+7&{NUrOg2YcH zQEfCsYUuGp?7=%xNv*+calw*UzZoZ*`q8z#lR4^0dz?(+S&(7rigj4V%-rMa+*?46 zCwQ+0{PG5L%gly@kpwbT0|d|$ZOoCd8we6&%el+gdvwNp-2HCU5FZWm&P3W)DQ~3k zqd*#WxFU%@q7OQ_sVpq7;t#mV;*aaIY`0_rG?QoUJjn)8$}%ahD`7h_<*m)Oap-T& zvjB0nOkTX<>aK}C)FX-_3KLMIP&|y$b(Z)7=8?i`!v>6}xL6*^Ozi{KpNj^SxUBAV zCi5`nq&2*&HUF#%J^+MN?Rel%9XNUNaHWpVhsyO>Xb~ePbhVJtaLnV)`8q#I_%q{W ziNJ9S1D9~2j-LTZbJ&hGQKRKk(fH2I*MYo$>v7i9FSN%qd1yQFWC@Al+d9?~BLSbA zKd(h6-5yrzArRu5LZlHdX~p)Igg-}q%9p0J}U}GrsF`QnPY$qk6yx!;Tnc& zn?ZzY*7mVC&Ct#~_>S5@Y4SGF;|KWdN#`~_(W19lxo1*(@$$Z@oBqM{0cV|boB zmyG&`7WO1_?9m6xSqhB(-&tYFYfCF`xpwknwkT)nIH9RLXIyYJKG#vT9WN?}NC3`1 zun(2@E8NzB=jS~aioP_6Q%jk&W|PZUOm&p|=*kf6eG}*XRwS8>NN^e+ za>(q;9y;|*PUih1e{KJ=l)+8}(RaL%^ilw*xSZp{uz|Lk>(o0`KrM36-b5)zrfCOe zl=Oq7#AydbgR-nXNZ%-zI+Hi&rvZB4*%}as6fE}&sFQVuf96!Y6?hTFD947qUSF9URgATGPMO*V@!FRV_Wjm8Brq`V~ zu4+!2qOFv>y#nn~og)R3l2=%aeV0c?E#Z z|HLE*w zKDOLtnE_sU^j+7SutOgaKURf+rGy&W9D(QMYwGe-7n3)YNI7>9w6W`*xeyNlDS&5iy zw|CIBGUPQiHy`9-`1Ohgw-sc$Ug|*Ty+C5rI7CRG{h`b!;gEiGxx2icEH?&$r(r56w(qswO*}bLOLxx;GUof6~EZARO*(;g< z6T)E3apUl$B$B2}<0nTlA<@Ua-JE(NOup|}5NllhLaMA$0MA~TUg5X`-*9aSB^X%k2^toZM@u9v4Zh=epBU?b^OWy*$Ffy5n)26dWEe5( zEJ|Js9krwur+Y!U?obJ59Ub@$%lyT>;#)UIMEeGg19Y`4o+Ki4pGvyeB)BD7Wt&i} zlVRZ6D7h!rE*5-_4`?*>2?hOdx^f0&KY3W1t}fepxHq3ET^)h>l& zmc>Gz@dUE&#QY)$qzCy3^ERElS}g;h$?p zI0Rz|!zshzr31P|ZX;`^c)fmprmhbk!GWPr!vCGh5Ca{~_ryt?I{@W)iZFhOV(v_m zRhV<{wkTmMl3$03zKFbk^7(En0YTAYPAHFOOr|ngGMOL%d zt-(S*NRa-pKF=p82p2y}s2DZ~4rRy*nmpm%&Gi0YEUXsxAb9Bz1FA%ZRkcvrQmMr< z$ad%2hyCp?`Plv3(iSx-zj)TaO_AwBdvauFuDX5(nyP@e}$b5fr*=SXmJf4+;Z_5d%TT2ua5*bMk8y9 z{7BfdFLu4u^US%)BSxKp^z%0g&x`_3BgJ2ma%th#yyTgJD~vPWA7q(jPNY5hej$|1 z!yTnsVn%K=Tyy)mA-`a-lCjsrgG}+oF|sF)LRB?9czp3Q;|QL$Ij)})&c;rB^|4#?W0 z&fGo%EVwk^g9M&>FxLj(b!LXZeJ{ogAHg6}8VxUMfQxHtW*&RP-=wYiGe1Amy6^`2 zOdj&}SRP|kK6-!lM)7oiu|*%t9-SWU@^tZm3w?O+5KKXK8KwX@5UusB(ah`Gx-3P3fq)oQ@NyPZ|+6BA8g8p4N=@UO$G#;&>9TA?@A4s41vSTK|o_IaN=B{ z#;)woT6uY1@G}ey`&7_fZbZkZs(dWwd)E2oD<)3jV%02Fc;CE)Lj> z&VHT2NF?HC5mKBvE(@h$g-=%0d%+Xj+~&-gIa(rU_HzKe7kaFZa}X^YP5zYc^E{Xw z;n3%G8FG-qMcVdqj|s$RAamYC-CK7PtSC_n`dWf+|I^lpcv|lbrrtCl9zc)90rav< zh+xGvv!ehlI!vb`U*8{ruN(7-cqLQ~d3?X6oYeU_?8_#vP)Aoh)#{^P5aQ%h^WW@$ z`jdtlEFot`|B6-Fn41`}9PCALNwz9j`SIl=F0N*y8jVglwBzY-`5T%wgvxYo;uAZ^ zb{;}dokLhjNV5SA*3njV1}qTnOb`Anx^C2+)>tHrWYltLsW=UX%U=~`!btPa;bw4E z&}mgHf-3MHA5Q2?s}+DT1_w5}&o2+t6au&C1&&;jR1h65j(_Kb%M`~nwm}KJ8|XY+ zmaE1$lc;q^jE}tzU&m$ze}VseoYD1o=*mHmdF04#ys&1<86aiq@5=k;T2;8LVDe30 z;`38K=H4O_BGxaMq(VghE9hcNv61IJV+!45SxV9#nsIgcCyCE;zWsHjEO&`-XsV6I zzECVr&3YaHm_Lo`nu6NP$ONQ$iNaP88p!J2Z_B>UI3uDzzMsWi>voU>G+)FGn0oIt z=1|S!rsH%{T0P*S-FpifUb<%}+dovalFP{uZ$R^LG!Oj~LiH#yrVBj0$EPjoc397c ziAhiOVWD!-RxkzAj*q`H@^|&H=L-nTqa!%JIdLPDXN)rCPD<)kkpu30vrrg)@nQW0l4VTRG%OI?( z5;UrV6tn!gu=GG=!1OM>y!>^V(pp3C^Di3Y>tXKRt)#%k!x;9aW=CH1hs6hsgsb=S zye%eSmyd4pc_T9MvX|1A5`|Rv5JW0DsH^l+q(PiPlt1!0gC+h3LhoVREH*Q?pL5#& zGPM$QenEs4czyc(70_wz0EiQ=vznEu`d~`k_8JMSxaK|EAco&aL;VSP-`2LQS8Ull z3KU2W<8Fp?l$+~rEJascANoGMqJfTu1Gn6XMDqh)?S1^|!D@N5H(}YP7t)_(pGZrh zpnk0O_u^tHsUw7*YQVQTu5vf^ILwM~T=GZ5M@_l09b4oo`r!b($qL&|hZWVC4u{QM z8Hzpm-sL0h_Ddfn%cUR&?gUM0xz10ocm@mU*oL_Eo|vu*-2P=Yj6Fy73Fx)md-N|X zuj>k_Gb75hdfB_HJMs1HOGTDP_`03*!<}j6XFh_ez97E$;h*K<%faEd$x zKr?rJY%368-Lc7EvdqG3zi`m`ZfC^E52dPwq+X)_>8XsXn)nb#ug6hvs-1R+W-L;T z;n^ONP!u`f3-odbWpU$shGI)CX+AYI2^L~0KW+5&Gp9nsaK3Bqh+K|ajwJV?t#=W; z8jIjJ;skzmH&Ozd;dHyaR`+hctI6B}jw2!sFw`DZ%wvjb3x)oSZpGR^shq{xZw8c$ zf)haDKl&F59QuOa5PF4hwA7C8d%T1KzK7Vyel(D0casR{dc{*AEp|L0`W`I-gNPc; z=+3A+j>AoaeLF`_8+)?JS9|Cwzen=jzG0VxI~M#_;y!Tyjh#BvHG~` zX;Zd{F{1~$nM>4 zcJ$1c*gt^4vpH$3=H3Ejf}U60sC@JR55gqO)%j}@CO=)DTn&qks|w+w6B=q}IKNI+ zjjnE8haMbqW~V9cD2#(|X?i^pDG9m7{B$L&8VO;Lz=)5RQpVkSxw|9a*^{+1W9`B1 zIWl4uLH_z>5LM8`7yV$$Kx6ICLOFG3*Ky+0_8a&8qb_&6M*@9WDpK)37Q^ZTwtEZ} zq61#^lO1mN{qz(;({}T?30;%)6fR&jkiY!|JX?+R`qNogvro18^HNjY&7ywVJ4Jpo zH!g=q11(*X?Mg;->gdehw_J9-zO59X>OZ_9I7;xY(V?COfXbY}xzN z_!*Fik6;+P&eV0oVp~0_uq}j>d-O}KSM!#fOtC?R z<<9D5rOh-jaNF~AM!h2H9~3y+D+>JSXsV)nidnytnZ12ny*TyLgYZ`c7x1#~<9v+z zQajZEoi;^Q&+7TrTo@cs0COZe&yms83C!HQ|81jK`@HTQ+VZBlRY>|skymVkwS-)b z%S+&Tz<$b54osK9;Qk1+@)hXKW`;;v5!7+q^PX@N$LH9(Kn(an5`F?tCN?&ib=8%w zB8cNk9|kgTtt>zCl*U=pVw^+gUH5-f*Ep-Ma3T-?Zrq&e!)4e8|B+5Wrd;UZw>)Kk z+s){rD_XlD+)pRC14)$X=hZ{=kty|il?;({MIm{K0Yv*~r801`W&W>G^3{|-Vj7)3 zdgUI>sNMkl0}N?^_9ra2?@jL;4L`1*x|{^wUnSV(1SUrZYRPvYaRxq?gfPGEi4)pu zapMnaefX0xD+jYtrgS3bh&QMYW(*2_oQQqwPoOx*Jbu#Qoj@zXRMK+hxeI;pE4;$O zV;nPCW9jGBMX)Q<4H$S?0dbfr_1N1{#{}qZFsz={&YCmlu3=Xx;4{6;cZNA{KvwT18<%;K=)sNP{I#=V1>xh+fwqK+@i z!P)&VREZt_6n)_wqg2&F4s>MZQP|M;@6wCoOsM#jm2Kd{_rr4@_q()ap71ySQi8S9 z){upWFWF|x&o8E-;AcCKy~^vZ)v@rmn{vwX31kgP{og)jF|ep%U`-}r{i&eVe_C_# zi5O{4*mdSk1hj|wz|Eo8GmTmXenxH}EBvwT=-TwHA$=(Ulakz_$hX7v)_dwCSr&`M z)PGe}j9u)8<*^8v!=c;Yt8maa=cAH!GmWmPaH!F@s2^Vfzt^FQ?*2e`QO>~hr$*i7 zr|C3d@V+vO2OK8?-e!)RF}c}f@UiL--7IC2ak)&ge!md#yyw8tP(6FyH9{bIv8A+% zbkaup9QX>QMcf%_!nIY&p;P?${x0!HeY?`9*F6ZN70ox50fp1A&;sT(P@ctDxw(|2 z$(rEd3l;F58fdKmNO%DPTLV_g@Q-U{aUwGX-)_T?NRM^n4Q^wf&JYTG~C;stTEW9yG63$r;P9zfa z6CQ;FJ30df+KUIBIU@iUsDWDwuxtewIAVk^f%{MEh!>gaph4X3j|q$#4002Q{`@Y= z@86@)8`iI54em(ZcUH3Mkao-o2KAAQklpInx7D%FL*4JQMrh};Hhs_5Tin)SmFDd~ zXXYjiC2#B`t<0>(%Je5P`c9Jz2eP+aq_tSXO+567GrXH77>oqnyhv#sa+k?v#T$^QsU2DAA^jgp7&)$#T@T6d@E-FjFo zR+hoDch3|yrCUF%M@=Wb7J7A0-a7Zr8S#rBxVO!H_uK`4O9D)qFb?L@@I=VsDsDmRM6>DN<`M~3330r-nNeA~Mq@%k+j>s7SQ|+Py&;{m7U9*+?vC53er~{u&P|_~N2MvIe7} z*zi!&rGpm{q1snFv?6v6J}gLce$=p`(=&RfdrzM_1#Tz;3>`EOO~S;sw~QGxJ~A}; zm`vnnqXi>alz?i_ucHrN0vh(~ z)4>kvA5c|@zb89)?qroq+}V`+S9@427INXj1+s4KT9VN#jV;-xUcG(IzM~k1B+!PM z@LWcp^m(I(5B0fy+Ekc~ZFIvEpjS$F=-;;w^y}Mus7|HWt&)oC9Lt$m_(L5bY8z=! z89Qn@bD^!(Un}P@+=89y@xJyzu-Q(jy_BZPcXUFS|cBLdxdxIgLzG9v%|B zcF=%+TDlVpBPRGq5g?;aZ|I$#24TTL-sm7Mz>w#F7Ky;twj&8>T2q>=k|wC|X$vGZ zF_x@Y_Bh$Ka|bEN%VTBH|JRyR8srO7t{zeG zePE_W-FSihORxNIgB+JInc(XsZZM>Xlnc3 zJyOU$^XHM5UwVOT*}R#pP^KXzt$j=#NotEJFW^*3JH>r{sb+jN((Ad6k&Ll zopqiZJ9d=p+xH8ru|o%G-g)y)Rz!2gv?*k8MjsL%70&iUFaqK^X?2zQ)liI}GJ8Hg zwDsj$rR-?1M)_=HsJ<_1wYCwVA%Fycn*?x!0O%aCEG2ra24CyF!a{-)^jg(ItxC2V zRck&rc#W3&38)e5HE_fjN_Jx*7VU{Nvl*>@M3+s`*+>=o5~xvF3H9XIXr?s=XY?gQ z2WGH}ngjdxCTS^2th8E0Xb>x~L?e>GE}d8}?i5c-8ZxY}t2@s|aX5R_YC}~6bMudbU2dPz$qMH2{iExG% zKtSzaqFbXF8SF)o*t`Q>LM6h`i%zXlPn@PrrE?LqfMVwkUaXX=9bWA3F8Dqr4V{po zf~EsCRIRJ)$>tEc+Ftdv7SB_Ne;-ABpx3EqMuh2u@Si6VfL-rR0$gVTga&CLG9(m& zHENda5__s1RqYg=LbgUHm+sQYrKgY(c_^4m2#cmsFLhMRY`|sJJn8$5zSoFM=!{BC zjA-^E+bD3^My*2r7@nUdUJgX{&x9Qw>bTaM1o&?dKuHPUfduf_TWE zTe6hbUTDF8^uBM`_kGv*-@du#oaZ^u^Sgid^4#}*uInUPTbXjOim(Czz+rA?WJ4Rt z2iGAc+PzH&-Afx-h-PO<0Kmq3aDjl#Y#{((6v5j$1vsHjA~6JCMKqS+j#CWrCDPCU zprso^L}R>h0bqBWC*Dt6a%Qz!w*Q28a0i_>qty+LFI`k+k{2GE@@$3liY1EvbK?5bT7q1{)ImabOiiRR{*I ztPWPwP=u>0!<1ANzz7&z0}4}z!j&K}I1;9cR8a%}^^>G&^T&E1ZH!F*(nWjHmh=h; zAR?jA;NW1zU?oL@zb6!~p+Vz7KoJNC4FMsA`URjvAbupNzZr~hB#b|v7=S1Efe#qb z?u5VqZAqG?f4bmH{7vge`YTMdfI&mhL?~PlcHq)4AQtl*M-24$`K26-f#Q5{zBs=C z5)BLgjU{>!0th58!hb{kd;6ah&_ats{nqhsZSnQ}t%4L_97J>DuYmkpG|4WMh=bbT zNQ6Lt49+-+W~S7EH$%UC~BWPZOWl?Ah-tWMP6aOHBGeQU8 zv?Xch28Y0uAaGSXI2@_0j8uWi!!(dE*q=}o0gLwt{TqsaL14-dINT1dj8s-bDyjVw zl$JDDbO8E)1!FNt4}!lhn&vXz7ww6I68$_S!M_8EG$i;C{Ar45)+znty1AjDwLifF z??c-l*_axD&5aFJ;To!{5QHN9m$@hu(%g>}fcC@S%#E}qY2i`C;I5>P@4JBgQES@6X?G>`D^bV3jA+4t!o@if43{z!|#TN^P_b+ ze_A_Jv43L)06aqGM*4OkW6L?A5q9=>IyFH&baIfVrH-qkcdjvDkK8)ODi?$1b|&IM zrj_w44yLAf$%2AwKAfLr8)Z2zG`Ni;Cphyin;R)&2t+ z3q7$x5i?+n&fLF-evDqQYt&`$;oj!c4Z>Oab=1!8IZl8aa-v%L#E2!?E*}b`6QWOM zUSW)4(_;VFqrzQxc<1)d2r(FI9cveZBBSE1sT`mh0dmWp$dQhn5bjE&>toDioTP68 zZGmulj6|^#y65CmiqPphdG^4Z9;gE3#ID7x#dPwz+h^9F1{bcW>SeQ#7aQ&5!Q5s{ z-@7Kh^R0m@m`iRMjAljfNdJg_Kn-Ad3L?uzza2Ak^HK$_H)wV zAbX;22|O_5?51sPIeh%_$f0ItCX3a3H7w*y_S=g1At;m8;Lk4l2*@l)#)eaZ_YiPs z>}zDSCs|65Jdc{ATE2Q}o>Hg^Qw9nL9)f}a2{qopDJKej6MYm%A>ezK4=B8suGm)j zt=d_RK|KBR$%#~kHjrNKcH%~CvBlK02e!x7OodPXkzd^4B@39%xPsei2YLn7K8>WU zl_nEqmTvnX*a583V<}?e$-$kI8F^{1El-(^(lf*XWWFc_QNDu0==5#ol~qPyb?nm7 zb}_(ary1}|H|M=6V)_HyA0v<=lW+p~n6Xa^Esyl>`SKk9c!c4o1al>m-i^NLM=+c) zV)+Js-qPSAH--uLv=GcV&zt2ONh+v8T(~lLdWBG zUUYzeN*KX?BNTw^0Q-&dMDxmf65O?S$4&SF|802$(%!r0sK-b8c{4#RolL+CC|4bb zG91)t=dW%r0rXb(QWBCfRb=)#?lBfg`NJ*_0ebTJqHAg0;cbHgpRUy;GSOZAxw^Qrla=FX_Y0x&9ho>IidEWlTUSs%~CmU>gI;;FHnc7L;E`=oGEYqvJ z7pzil-}$ipBP)B*n^LA>XH6ZmFLSDM(}DJ9ws?~ksvdHIYEEPW?^A{q$+SzrbzcrZ@y0Y&H3v4c-ElNnDeL({F7I;;f~_RVZcIIuDGoOBT)>SH;sZO9N3j_T` z8I4*;NCShci!2dEoi`mE?AG>{*y;+x1(hMfSw+*<{MnQk+nQ|Wp#B)j32dF%$$=cR znKY?5rj|sC++?UPXgB<;UJdI#O{=EV5ViOsH^&zi!*(*Ly7U~)r?!rQTTNq$vS43c z7C;(&<1pil4$k<~>3J;;MpTXale0vMgEI%i6!D!M8-;?1`a>v1OvlHJt1i`X?GN6Y zr|tR2g{cwWdGa(LvO3t$bY!>qOjN_2x$Gn}&&wFo0n1zy5K3EUt4J{ariaz6jW>dG zG^RH*g3~1DtfGC(g&P0;E9-W5ZVOxN1nNl8Af4{RAD_smea^|=sIz@25T&6N|9qmM z6%>4A&8ShcL=8ZK>jPsWoEU#bHMv%brS~SfqhubWu_GA)HmkB2kKvHb*!|arxX&i8 z5p_c6ELg4Vxd%dXdu3ANip4_{u5MINwIA!qC8SKW&T=ikoR8ncJ~;N`edsvkt)2Ja zvydjRHPMSTBS)!gsFVk8;%5aOovF<3OIH4aqJlNiuxF#WIZ?D;?CB}eDzM|z?f%N<{L#zbN5aosbmA}4 z)ye4Txvsr@{%c%*F5o4F=^r;#y)GvoY9(U!5+*!n2T^~>dY>R<mOTELs+2bdYX-_Yi({|=qmN^7fJq85G+G=41parGUq zEif`N0t*{Cl$~j>24FRmRU;Y{#ZI}!u#bIWB0r^`Pw417divr;Zo|hC1zBr3l-P$O ze6JsVlY3g8H{QP}YC7|Z1qd27pcJjWJ1!9~7Jq}gh3yN2p60Y#LeQt9r_t!6ZT?>v z^*pX7jg~pQP+cQ^xKa)fy#$;yV=p!ByOa#+>lRpT8fb$C#dn~sviow1a_)=QThO8w zEoG!IR&S1cke_%c{Rv7zujV_u*Z>r6`EvzX`+WU!J!qQF4S_Bs%lD+L5AHNr2fE)f zv=hl>=WMwbtDSzN5Z&2|vQy^AJ+I@$N&>%~vwn)}0roTVg-@7a-yK(fsDf_IOz7sX~0WGV#6x^JuFD-DEFHZ@g z;?>E+_bT0onBxvH%=wP@;4*Srr*<6}(p3|tP^j;eo9(fsaDi`F5uA4ZeQ+=r2e8zfiK|Y7os4AK~rc|Xc zSlNyn=zctBOM&dsvc23h84u4j_E-zIe7MXkbaB^BxvXZW`;psB86{Ss_*$(%UvYgs z@_pyf?x8yCdI7W}gnlp`K}Vm zWe?KEP+t03l<8LwwNy0HKYVc4CO>N64gtAejaL*%A6S~p8qmeGQ+<3gDY z1PyWy<(br)iw6b<{;VTN9M&c3YHNCSeMnHc44$}makC`t;jaI&(b3Vk@Reot&tBNO#8p>G7E&S_3UY>&$#F~f4RgbC@9=I=dCdM`gK)guZK&f zQkJHlNH88t8kU7kteaZxoL#qeap-7?p?m5(+4pjJylY^f zxUldF?{O=m4~_2;K2w}I!!3qf&pd7>qmEIb+$>t>*$l#@OlmT*K0T(@H->A#(wsx7 z9{0Lk+lJpz`4`Xd$RrtPTI|0P+i~*=v-Nwe-UzbJBowq$4j3- z&raX}V#>6&YMH8-L0e91ZIv?NZIXaT{&dYW;caoKOr*lV&^v^^w%3oGb0!X_7hRvJ zQu?~yDXSf3CvkIZX|C;9QuoTRC~d?2$ql7NkszNbfrgQQ>12;L=yu#-q1P5`is=`& zyS$bd;?f<+anQ6K+lYn z^;kU_Pa&w~mGKv~#$VX4zRS+1y_U6pPSY2@ZP%txFCqp8=}w0JK&2dEi}XSJ`a&Kz z*OPk@!>6KFZ%0BsX>D_MQ&^7QpAL@wUf|TnmXN;ilCAO;olairsB1aaZNKSwjo#J% zo;mFX_`HTW6f~U@y1?5c^<6{(sOSRUrSR$&vNNp|mcPD>*ZVK{-p^2p6xk2i*KRu7coik8Rzd0vqNzEq3r+3MQmtHjq$YIfjZ;`ax7M;| zSm07?YwaZL?1``=Yo6ewJR!)bTOA$807nqKv3R+h^5Mx`Z{70z=|as@HQuGQ)3(cU zXYh&#V${7zVbzaFKXHwiEI*-_qwjXREJ!&~+{U(xcSaKh^DbGb4H%e?B?|GRVXr)k zzL@ZBYx@NfxwszkwhpbGy0^kzzW8ppka6|GU}M>y=LyxNGrFs98jh1(Bcclm4DOfi zooz1>zeeigN?`?J3NGhxV_sr%r1 zJ-<(g>pd{$t`=Uhtl7DHeU{%u!PGVS?ohT{^~{jD4P+TcmmZ-5?X<9b*R;%h2m0gI zw@nw1p8Ge2NIxEZWUr3{?fw`nc05coFUPyQXMW{k@Wz5}-&_OX&1n}Gzmb)X$Ec0e z7ugse9|eFb^{$jU%jHovT94NfKYT#id`V(F>TS>W)n?4ZSk1$84R;_}YNU#Ma%QH8 zl&fB8oRs+l3fHIu zUimJ=m^6rsfljm_>1&W}hv$Q~gt5Jc#8`qXz;J}w!HvBjer$Y5XIp=4*^(uppcT9vvZq?gM+tEbPiBa6{AMK73Zu z0F+Clx&U$gY85=*wDsaMP<;;vw;)JdR1nbbxECj6x~U48eN}q+;)^|<{NZoyTUked zn5u;3Xz^Fu6GJC?&7;~GUuIDC@N6|HFv7LNWTp%w{-K%Aq3vJF<_lN$uP)Xs8cFwk zD4-4((UZZxgAOLtm0Ks}y<4~RRVvtr>edk%(n+b3x>*8%NL|u0H_l_0+-!DjkgvRu z`D4od@im|TH~u2cVY{Cd;2;ljnwh@86mB(kNQH?n;?Wlmmz$r)R}wOJUk_Pt_wxW8 zDRI*6k?k$iHfjT+b~^FiU?U>p2ltw6ftM8O!ea-f>2sTa9z9sPE9Z9NV!~~g@LHq!zygrVq*} zq#jSE-w5_5+Zb)e4m>!5I5MQLDI-iR6$~qLD*YzFMt JRBqrF^Iu$8TZ{kz literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png new file mode 100644 index 0000000000000000000000000000000000000000..272c6bcaf75c18a985b971284b11162a13a2cca0 GIT binary patch literal 20284 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 pu7$$YDiL Date: Sat, 11 Apr 2020 15:01:09 +0900 Subject: [PATCH 41/77] Add support for taikobigcircle and fix exception on missing layers --- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index af10944ee9..43d45ea1c9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -6,6 +6,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -26,13 +28,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, DrawableHitObject drawableHitObject) { - InternalChildren = new[] + Drawable getDrawableFor(string lookup) { - backgroundLayer = skin.GetAnimation("taikohitcircle", true, false), - skin.GetAnimation("taikohitcircleoverlay", true, false), - }; + const string normal_hit = "taikohit"; + const string big_hit = "taikobig"; + + string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit; + + return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? skin.GetAnimation($"{normal_hit}{lookup}", true, false); + } + + // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer + AddInternal(backgroundLayer = getDrawableFor("circle")); + + var foregroundLayer = getDrawableFor("circleoverlay"); + if (foregroundLayer != null) + AddInternal(foregroundLayer); // animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). // for now just stop at first frame for sanity. From 3d5a622db7b0dbf8e4984c1ea57dba56c1d7393f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 15:04:58 +0900 Subject: [PATCH 42/77] Tidy up comments --- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index 43d45ea1c9..80bf97936d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -37,18 +37,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit; - return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? skin.GetAnimation($"{normal_hit}{lookup}", true, false); + return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? + // fallback to regular size if "big" version doesn't exist. + skin.GetAnimation($"{normal_hit}{lookup}", true, false); } - // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer + // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. AddInternal(backgroundLayer = getDrawableFor("circle")); var foregroundLayer = getDrawableFor("circleoverlay"); if (foregroundLayer != null) AddInternal(foregroundLayer); - // animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). - // for now just stop at first frame for sanity. + // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). + // For now just stop at first frame for sanity. foreach (var c in InternalChildren) { (c as IFramedAnimation)?.Stop(); @@ -66,8 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning { base.Update(); - // not all skins (including the default osu-stable) have similar sizes for hitcircle and hitcircleoverlay. - // this ensures they are scaled relative to each other but also match the expected DrawableHit size. + // Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay". + // This ensures they are scaled relative to each other but also match the expected DrawableHit size. foreach (var c in InternalChildren) c.Scale = new Vector2(DrawWidth / 128); } From f274ec297ce23e79eff1d2a64f745e1396b0c280 Mon Sep 17 00:00:00 2001 From: Fire937 Date: Sun, 12 Apr 2020 01:33:25 +0200 Subject: [PATCH 43/77] Add positional sound support for all rulesets The SamplePlaybackBalance is calculated in a way that the balance remains between -0.4 and 0.4. Positional sound is not supported in osu!taiko. --- .../Drawables/DrawableCatchHitObject.cs | 2 ++ .../Drawables/DrawableManiaHitObject.cs | 16 ++++++++++++ .../Objects/Drawables/DrawableOsuHitObject.cs | 2 ++ .../Objects/Drawables/DrawableHitObject.cs | 26 ++++++------------- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 6844be5941..e726d6eff5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -70,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; + protected override float SamplePlaybackBalance => 0.8f * HitObject.X - 0.4f; + protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 5bfa07bd14..76e9695855 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -5,8 +5,10 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -24,6 +26,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); + protected override float SamplePlaybackBalance + { + get + { + CompositeDrawable stage = this; + while (!(stage is Stage)) + stage = stage.Parent; + + var columnCount = ((Stage)stage).Columns.Count; + var columnIndex = HitObject.Column; + return 0.8f * columnIndex / (columnCount - 1) - 0.4f; + } + } + protected DrawableManiaHitObject(ManiaHitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index a677cb6a72..c5a4491b2d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. public override bool HandlePositionalInput => true; + protected override float SamplePlaybackBalance => (HitObject.X / 512f - 0.5f) * 0.8f; + protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 30a9106ddc..ef32dc1560 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -33,11 +33,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public readonly Bindable AccentColour = new Bindable(Color4.Gray); - /// - /// The stereo balance of the samples if the Positional hitsounds setting is set. - /// - private readonly BindableDouble positionalSoundAdjustment = new BindableDouble(); - protected SkinnableSound Samples { get; private set; } protected virtual IEnumerable GetSamples() => HitObject.Samples; @@ -94,7 +89,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// The stereo balance of the samples played if Positional hitsounds is set. /// - protected virtual float PositionalSound => (Position.X / 512f - 0.5f) * 0.8f; + protected virtual float SamplePlaybackBalance => 0; + + private readonly BindableDouble samplePlaybackBalanceAdjustment = new BindableDouble(); private BindableList samplesBindable; private Bindable startTimeBindable; @@ -119,6 +116,7 @@ namespace osu.Game.Rulesets.Objects.Drawables [BackgroundDependencyLoader] private void load(OsuConfigManager config) { + userPositionalHitSounds = config.GetBindable(OsuSetting.PositionalHitSounds); var judgement = HitObject.CreateJudgement(); Result = CreateResult(judgement); @@ -126,16 +124,6 @@ namespace osu.Game.Rulesets.Objects.Drawables throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); loadSamples(); - - userPositionalHitSounds = config.GetBindable(OsuSetting.PositionalHitSounds); - userPositionalHitSounds.BindValueChanged(positional => - { - if (positional.NewValue) - Samples?.AddAdjustment(AdjustableProperty.Balance, positionalSoundAdjustment); - else - Samples?.RemoveAdjustment(AdjustableProperty.Balance, positionalSoundAdjustment); - }); - userPositionalHitSounds.TriggerChange(); } protected override void LoadComplete() @@ -179,7 +167,9 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - AddInternal(Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)))); + Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); + Samples.AddAdjustment(AdjustableProperty.Balance, samplePlaybackBalanceAdjustment); + AddInternal(Samples); } private void onDefaultsApplied() => apply(HitObject); @@ -378,7 +368,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public virtual void PlaySamples() { - positionalSoundAdjustment.Value = PositionalSound; + samplePlaybackBalanceAdjustment.Value = userPositionalHitSounds.Value ? SamplePlaybackBalance : 0; Samples?.Play(); } From 7a9ee907bf4d862c1e49188a8c835a2412977fb9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Apr 2020 07:34:58 +0300 Subject: [PATCH 44/77] Fix incorrect button state in some cases --- osu.Game/Overlays/OverlayScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index a6c687f28f..a8f2a0ce6c 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -60,7 +60,7 @@ namespace osu.Game.Overlays return; currentTarget = Target; - Button.State = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } public class ScrollToTopButton : OsuHoverContainer From c3f0475748f910172c5161db4dbd62afc3eb0c28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Apr 2020 17:40:22 +0900 Subject: [PATCH 45/77] Make CirclePiece abstract --- .../Objects/Drawables/Pieces/CirclePiece.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index 70fe4b7bb2..6ca77e666d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces /// for a usage example. /// /// - public class CirclePiece : BeatSyncedContainer + public abstract class CirclePiece : BeatSyncedContainer { public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_BORDER = 8; @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public Box FlashBox; - public CirclePiece() + protected CirclePiece() { RelativeSizeAxes = Axes.Both; From c5d6c7728a512ab6a85857e391f7841afedb255f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Apr 2020 18:29:25 +0900 Subject: [PATCH 46/77] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 5b200ee104..723844155f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7cf1272611..0732e6090d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c58a431e80..d7006761be 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 07dc2773218e4e65e6c388e7210a49a8a6a8f8ac Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Sun, 12 Apr 2020 14:55:42 +0200 Subject: [PATCH 47/77] Remove unused changelog comments class --- .../Online/TestSceneChangelogOverlay.cs | 1 - osu.Game/Overlays/Changelog/Comments.cs | 79 ------------------- 2 files changed, 80 deletions(-) delete mode 100644 osu.Game/Overlays/Changelog/Comments.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 864fd31a0f..22d20f7098 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -24,7 +24,6 @@ namespace osu.Game.Tests.Visual.Online typeof(ChangelogListing), typeof(ChangelogSingleBuild), typeof(ChangelogBuild), - typeof(Comments), }; protected override bool UseOnlineAPI => true; diff --git a/osu.Game/Overlays/Changelog/Comments.cs b/osu.Game/Overlays/Changelog/Comments.cs deleted file mode 100644 index 4cf39e7b44..0000000000 --- a/osu.Game/Overlays/Changelog/Comments.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; -using osuTK.Graphics; - -namespace osu.Game.Overlays.Changelog -{ - public class Comments : CompositeDrawable - { - private readonly APIChangelogBuild build; - - public Comments(APIChangelogBuild build) - { - this.build = build; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - Padding = new MarginPadding - { - Horizontal = 50, - Vertical = 20, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - LinkFlowContainer text; - - InternalChildren = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreyVioletDarker - }, - }, - text = new LinkFlowContainer(t => - { - t.Colour = colours.PinkLighter; - t.Font = OsuFont.Default.With(size: 14); - }) - { - Padding = new MarginPadding(20), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - }; - - text.AddParagraph("Got feedback?", t => - { - t.Colour = Color4.White; - t.Font = OsuFont.Default.With(italics: true, size: 20); - t.Padding = new MarginPadding { Bottom = 20 }; - }); - - text.AddParagraph("We would love to hear what you think of this update! "); - text.AddIcon(FontAwesome.Regular.GrinHearts); - - text.AddParagraph("Please visit the "); - text.AddLink("web version", $"{build.Url}#comments"); - text.AddText(" of this changelog to leave any comments."); - } - } -} From ecd25e567d8803c548e273b11aac15f333e55da6 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 16:00:05 +0300 Subject: [PATCH 48/77] Present selected difficulty --- osu.Game/OsuGame.cs | 12 ++++++++---- osu.Game/Overlays/BeatmapSet/Header.cs | 3 ++- osu.Game/Overlays/Direct/PanelDownloadButton.cs | 10 +++++++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1b2fd658f4..113e9bbe24 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -315,8 +315,12 @@ namespace osu.Game /// The user should have already requested this interactively. /// /// The beatmap to select. - public void PresentBeatmap(BeatmapSetInfo beatmap) + /// Predicate used to find a difficulty to select + public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate findPredicate = null) { + // Use this predicate if non was provided. This will try to find some difficulty from current ruleset so we wouldn't have to change rulesets + findPredicate ??= b => b.Ruleset.Equals(Ruleset.Value); + var databasedSet = beatmap.OnlineBeatmapSetID != null ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID) : BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash); @@ -334,13 +338,13 @@ namespace osu.Game menuScreen.LoadToSolo(); // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash) + if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && findPredicate(Beatmap.Value.BeatmapInfo)) { return; } - // Use first beatmap available for current ruleset, else switch ruleset. - var first = databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First(); + // Find first beatmap that matches our predicate. + var first = databasedSet.Beatmaps.Find(findPredicate) ?? databasedSet.Beatmaps.First(); Ruleset.Value = first.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first); diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 29c259b7f8..a60b9e04b1 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -277,7 +277,8 @@ namespace osu.Game.Overlays.BeatmapSet downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value) { Width = 50, - RelativeSizeAxes = Axes.Y + RelativeSizeAxes = Axes.Y, + CurrentBeatmap = Picker.Beatmap.GetBoundCopy() }; break; diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index 1b3657f010..eae2f3353c 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; @@ -16,6 +18,8 @@ namespace osu.Game.Overlays.Direct private readonly bool noVideo; + public Bindable CurrentBeatmap = new Bindable(); + private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; @@ -62,7 +66,11 @@ namespace osu.Game.Overlays.Direct break; case DownloadState.LocallyAvailable: - game?.PresentBeatmap(BeatmapSet.Value); + Predicate findPredicate = null; + if (CurrentBeatmap.Value != null) + findPredicate = b => b.OnlineBeatmapID == CurrentBeatmap.Value.OnlineBeatmapID; + + game?.PresentBeatmap(BeatmapSet.Value, findPredicate); break; default: From ed28e8c8f5f6d92f7f06034800fd9df82f688ef7 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 19:38:09 +0300 Subject: [PATCH 49/77] Rename param --- osu.Game/OsuGame.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 113e9bbe24..5e93d760e3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -315,11 +315,14 @@ namespace osu.Game /// The user should have already requested this interactively. /// /// The beatmap to select. - /// Predicate used to find a difficulty to select - public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate findPredicate = null) + /// + /// Optional predicate used to try and find a difficulty to select. + /// If omitted, this will try to present the first beatmap from the current ruleset. + /// In case of failure the first difficulty of the set will be presented, ignoring the predicate. + /// + public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { - // Use this predicate if non was provided. This will try to find some difficulty from current ruleset so we wouldn't have to change rulesets - findPredicate ??= b => b.Ruleset.Equals(Ruleset.Value); + difficultyCriteria ??= b => b.Ruleset.Equals(Ruleset.Value); var databasedSet = beatmap.OnlineBeatmapSetID != null ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID) @@ -338,13 +341,13 @@ namespace osu.Game menuScreen.LoadToSolo(); // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && findPredicate(Beatmap.Value.BeatmapInfo)) + if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && difficultyCriteria(Beatmap.Value.BeatmapInfo)) { return; } // Find first beatmap that matches our predicate. - var first = databasedSet.Beatmaps.Find(findPredicate) ?? databasedSet.Beatmaps.First(); + var first = databasedSet.Beatmaps.Find(difficultyCriteria) ?? databasedSet.Beatmaps.First(); Ruleset.Value = first.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first); From 3b9e0fa67def20a8e9177088147cbfff4536cc07 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 19:42:28 +0300 Subject: [PATCH 50/77] Use readonly IBindable --- osu.Game/Overlays/BeatmapSet/Header.cs | 7 ++++--- osu.Game/Overlays/Direct/PanelDownloadButton.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index a60b9e04b1..4e57bfa688 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -274,12 +274,13 @@ namespace osu.Game.Overlays.BeatmapSet { case DownloadState.LocallyAvailable: // temporary for UX until new design is implemented. - downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value) + PanelDownloadButton panelButton; + downloadButtonsContainer.Child = panelButton = new PanelDownloadButton(BeatmapSet.Value) { Width = 50, - RelativeSizeAxes = Axes.Y, - CurrentBeatmap = Picker.Beatmap.GetBoundCopy() + RelativeSizeAxes = Axes.Y }; + panelButton.CurrentBeatmap.BindTo(Picker.Beatmap); break; case DownloadState.Downloading: diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index eae2f3353c..6fe174438b 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Direct private readonly bool noVideo; - public Bindable CurrentBeatmap = new Bindable(); + public readonly IBindable CurrentBeatmap = new Bindable(); private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; From 1cf240b5fff1c269f9e668970a4046d963521b41 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 20:04:25 +0300 Subject: [PATCH 51/77] Test new predicate behaviour --- .../Navigation/TestScenePresentBeatmap.cs | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 909409835c..27f5b29738 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -20,26 +20,30 @@ namespace osu.Game.Tests.Visual.Navigation public void TestFromMainMenu() { var firstImport = importBeatmap(1); + var secondimport = importBeatmap(3); + presentAndConfirm(firstImport); - - AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); - AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - - var secondimport = importBeatmap(2); + returnToMenu(); presentAndConfirm(secondimport); + returnToMenu(); + presentSecondDifficultyAndConfirm(firstImport, 1); + returnToMenu(); + presentSecondDifficultyAndConfirm(secondimport, 3); } [Test] public void TestFromMainMenuDifferentRuleset() { var firstImport = importBeatmap(1); + var secondimport = importBeatmap(3, new ManiaRuleset().RulesetInfo); + presentAndConfirm(firstImport); - - AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); - AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - - var secondimport = importBeatmap(2, new ManiaRuleset().RulesetInfo); + returnToMenu(); presentAndConfirm(secondimport); + returnToMenu(); + presentSecondDifficultyAndConfirm(firstImport, 1); + returnToMenu(); + presentSecondDifficultyAndConfirm(secondimport, 3); } [Test] @@ -48,8 +52,11 @@ namespace osu.Game.Tests.Visual.Navigation var firstImport = importBeatmap(1); presentAndConfirm(firstImport); - var secondimport = importBeatmap(2); + var secondimport = importBeatmap(3); presentAndConfirm(secondimport); + + presentSecondDifficultyAndConfirm(firstImport, 1); + presentSecondDifficultyAndConfirm(secondimport, 3); } [Test] @@ -58,8 +65,17 @@ namespace osu.Game.Tests.Visual.Navigation var firstImport = importBeatmap(1); presentAndConfirm(firstImport); - var secondimport = importBeatmap(2, new ManiaRuleset().RulesetInfo); + var secondimport = importBeatmap(3, new ManiaRuleset().RulesetInfo); presentAndConfirm(secondimport); + + presentSecondDifficultyAndConfirm(firstImport, 1); + presentSecondDifficultyAndConfirm(secondimport, 3); + } + + private void returnToMenu() + { + AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); + AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); } private Func importBeatmap(int i, RulesetInfo ruleset = null) @@ -89,6 +105,13 @@ namespace osu.Game.Tests.Visual.Navigation BaseDifficulty = difficulty, Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, + new BeatmapInfo + { + OnlineBeatmapID = i * 2048, + Metadata = metadata, + BaseDifficulty = difficulty, + Ruleset = ruleset ?? new OsuRuleset().RulesetInfo + }, } }).Result; }); @@ -106,5 +129,15 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.ID == getImport().ID); AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID); } + + private void presentSecondDifficultyAndConfirm(Func getImport, int importedID) + { + Predicate pred = b => b.OnlineBeatmapID == importedID * 2048; + AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred)); + + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); + AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID * 2048); + AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID); + } } } From 3efb4aba25803c36a2f50401808a389f3b082f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 12 Apr 2020 19:48:15 +0200 Subject: [PATCH 52/77] Use BindTarget --- osu.Game/Overlays/BeatmapSet/Header.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 4e57bfa688..a03613f955 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -274,13 +274,12 @@ namespace osu.Game.Overlays.BeatmapSet { case DownloadState.LocallyAvailable: // temporary for UX until new design is implemented. - PanelDownloadButton panelButton; - downloadButtonsContainer.Child = panelButton = new PanelDownloadButton(BeatmapSet.Value) + downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value) { Width = 50, - RelativeSizeAxes = Axes.Y + RelativeSizeAxes = Axes.Y, + CurrentBeatmap = { BindTarget = Picker.Beatmap } }; - panelButton.CurrentBeatmap.BindTo(Picker.Beatmap); break; case DownloadState.Downloading: From 65b96079a05f46de50dd1f0f191bed8389cdf62f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 13:00:03 +0900 Subject: [PATCH 53/77] Move dampening to base implementation and change range to 0..1 --- .../Objects/Drawables/DrawableCatchHitObject.cs | 2 +- .../Objects/Drawables/DrawableManiaHitObject.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 3 ++- .../Objects/Drawables/DrawableHitObject.cs | 16 +++++++++++----- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index e726d6eff5..b12cdd4ccb 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; - protected override float SamplePlaybackBalance => 0.8f * HitObject.X - 0.4f; + protected override float SamplePlaybackPosition => HitObject.X; protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 76e9695855..ce56fd222c 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); - protected override float SamplePlaybackBalance + protected override float SamplePlaybackPosition { get { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 068d666c8e..8308c0c576 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. public override bool HandlePositionalInput => true; - protected override float SamplePlaybackBalance => (HitObject.X / 512f - 0.5f) * 0.8f; + protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X; /// /// Whether this can be hit. diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index d6e231424b..b14927bcd5 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -87,11 +87,15 @@ namespace osu.Game.Rulesets.Objects.Drawables public JudgementResult Result { get; private set; } /// - /// The stereo balance of the samples played if Positional hitsounds is set. + /// The relative X position of this hit object for sample playback balance adjustment. /// - protected virtual float SamplePlaybackBalance => 0; + /// + /// This is a range of 0..1 (0 for far-left, 0.5 for centre, 1 for far-right). + /// Dampening is post-applied to ensure the effect is not too intense. + /// + protected virtual float SamplePlaybackPosition => 0.5f; - private readonly BindableDouble samplePlaybackBalanceAdjustment = new BindableDouble(); + private readonly BindableDouble balanceAdjust = new BindableDouble(); private BindableList samplesBindable; private Bindable startTimeBindable; @@ -168,7 +172,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); - Samples.AddAdjustment(AdjustableProperty.Balance, samplePlaybackBalanceAdjustment); + Samples.AddAdjustment(AdjustableProperty.Balance, balanceAdjust); AddInternal(Samples); } @@ -368,7 +372,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public virtual void PlaySamples() { - samplePlaybackBalanceAdjustment.Value = userPositionalHitSounds.Value ? SamplePlaybackBalance : 0; + const float balance_adjust_amount = 0.4f; + + balanceAdjust.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0); Samples?.Play(); } From cdff6060d3eccdf3ed36df6ce0d3aa26dc411a17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 13:01:27 +0900 Subject: [PATCH 54/77] Remove recursive hierarchy traversal for mania sample balance --- .../Objects/Drawables/DrawableManiaHitObject.cs | 16 +++++++++------- osu.Game/Rulesets/UI/Playfield.cs | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index ce56fd222c..a708adb493 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -5,10 +5,10 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -26,17 +26,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); + [Resolved(canBeNull: true)] + private Playfield playfield { get; set; } + protected override float SamplePlaybackPosition { get { - CompositeDrawable stage = this; - while (!(stage is Stage)) - stage = stage.Parent; + var columns = (playfield as ManiaPlayfield)?.TotalColumns; - var columnCount = ((Stage)stage).Columns.Count; - var columnIndex = HitObject.Column; - return 0.8f * columnIndex / (columnCount - 1) - 0.4f; + if (columns == null) + return base.SamplePlaybackPosition; + + return (float)HitObject.Column / columns.Value; } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index c52183f3f2..fc6906560b 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -15,6 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.UI { + [Cached] public abstract class Playfield : CompositeDrawable { /// From c51bad0e35e4a08d3e0e159e103884bbf709e85d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 13:42:21 +0900 Subject: [PATCH 55/77] Cache ManiaPlayfield instead --- .../Objects/Drawables/DrawableManiaHitObject.cs | 9 +++------ osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 2 ++ osu.Game/Rulesets/UI/Playfield.cs | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index a708adb493..88888001b4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -27,18 +26,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); [Resolved(canBeNull: true)] - private Playfield playfield { get; set; } + private ManiaPlayfield playfield { get; set; } protected override float SamplePlaybackPosition { get { - var columns = (playfield as ManiaPlayfield)?.TotalColumns; - - if (columns == null) + if (playfield == null) return base.SamplePlaybackPosition; - return (float)HitObject.Column / columns.Value; + return (float)HitObject.Column / playfield.TotalColumns; } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index c2eb48b774..2dec468654 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -14,6 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.UI { + [Cached] public class ManiaPlayfield : ScrollingPlayfield { private readonly List stages = new List(); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index fc6906560b..c52183f3f2 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -15,7 +15,6 @@ using osuTK; namespace osu.Game.Rulesets.UI { - [Cached] public abstract class Playfield : CompositeDrawable { /// From f38b64d20177c2010b415f8a3bae39fbf29e0303 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 13:57:15 +0900 Subject: [PATCH 56/77] Fix placement blueprints handling double clicks --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index ea77a6091a..fb1eb7adbf 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -106,6 +106,9 @@ namespace osu.Game.Rulesets.Edit case ScrollEvent _: return false; + case DoubleClickEvent _: + return false; + case MouseButtonEvent _: return true; From e17d5bdbaf3a6ad45b4a60af9ab9a168cf0d0406 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 13:57:40 +0900 Subject: [PATCH 57/77] Improve red slider control point placement logic --- .../Sliders/SliderPlacementBlueprint.cs | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index a780653796..be43515269 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -23,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private SliderBodyPiece bodyPiece; private HitCirclePiece headCirclePiece; private HitCirclePiece tailCirclePiece; + private PathControlPointVisualiser controlPointVisualiser; private InputManager inputManager; @@ -51,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders bodyPiece = new SliderBodyPiece(), headCirclePiece = new HitCirclePiece(), tailCirclePiece = new HitCirclePiece(), - new PathControlPointVisualiser(HitObject, false) + controlPointVisualiser = new PathControlPointVisualiser(HitObject, false) }; setState(PlacementState.Initial); @@ -91,17 +95,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders break; case PlacementState.Body: - switch (e.Button) - { - case MouseButton.Left: - ensureCursor(); + if (e.Button != MouseButton.Left) + break; - // Detatch the cursor - cursor = null; - break; + // Find the last non-cursor control point and the respective drawable piece + var lastPoint = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor); + var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == lastPoint); + + if (lastPiece?.IsHovered == true) + { + Debug.Assert(lastPoint != null); + + segmentStart = lastPoint; + segmentStart.Type.Value = PathType.Linear; + + currentSegmentLength = 1; + } + else + { + ensureCursor(); + cursor = null; // Detatch the cursor } - break; + return true; } return true; @@ -114,16 +130,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.OnMouseUp(e); } - protected override bool OnDoubleClick(DoubleClickEvent e) - { - // Todo: This should all not occur on double click, but rather if the previous control point is hovered. - segmentStart = HitObject.Path.ControlPoints[^1]; - segmentStart.Type.Value = PathType.Linear; - - currentSegmentLength = 1; - return true; - } - private void beginCurve() { BeginPlacement(commitStart: true); @@ -169,6 +175,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders currentSegmentLength++; updatePathType(); + + Logger.Log("Set cursor"); } } From 99fa1458470fe7fc031e3ae4e422cf2f1ad0d73b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 08:38:34 +0300 Subject: [PATCH 58/77] Add test for potential failing case --- .../UserInterface/TestSceneOverlayScrollContainer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 4205d65100..fd3b6ed3ab 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void SetUp() => Schedule(() => { - Add(scroll = new OverlayScrollContainer + Child = scroll = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, Child = new Container @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.UserInterface Colour = Color4.Gray } } - }); + }; invocationCount = 0; @@ -62,6 +62,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("scroll to start", () => scroll.ScrollToStart(false)); AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); + + AddStep("scroll to 250", () => scroll.ScrollTo(500)); + AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); + AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); } [Test] From 142cddfb10d46a1392dda590ac5fb1864c82bf7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 15:13:35 +0900 Subject: [PATCH 59/77] Rename CurrentBeatmap to SelectedBeatmap --- osu.Game/Overlays/BeatmapSet/Header.cs | 2 +- osu.Game/Overlays/Direct/PanelDownloadButton.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index a03613f955..11dc424183 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -278,7 +278,7 @@ namespace osu.Game.Overlays.BeatmapSet { Width = 50, RelativeSizeAxes = Axes.Y, - CurrentBeatmap = { BindTarget = Picker.Beatmap } + SelectedBeatmap = { BindTarget = Picker.Beatmap } }; break; diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index 6fe174438b..08e3ed9b38 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -18,7 +18,10 @@ namespace osu.Game.Overlays.Direct private readonly bool noVideo; - public readonly IBindable CurrentBeatmap = new Bindable(); + /// + /// Currently selected beatmap. Used to present the correct difficulty after completing a download. + /// + public readonly IBindable SelectedBeatmap = new Bindable(); private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; @@ -67,8 +70,8 @@ namespace osu.Game.Overlays.Direct case DownloadState.LocallyAvailable: Predicate findPredicate = null; - if (CurrentBeatmap.Value != null) - findPredicate = b => b.OnlineBeatmapID == CurrentBeatmap.Value.OnlineBeatmapID; + if (SelectedBeatmap.Value != null) + findPredicate = b => b.OnlineBeatmapID == SelectedBeatmap.Value.OnlineBeatmapID; game?.PresentBeatmap(BeatmapSet.Value, findPredicate); break; From 2c20328a70cfd3f5f2722b226346feeabad633ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 15:31:46 +0900 Subject: [PATCH 60/77] Rework control point placement for better progression --- .../Sliders/SliderPlacementBlueprint.cs | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index be43515269..9af972dbce 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -3,11 +3,11 @@ using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; -using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -77,11 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders break; case PlacementState.Body: - ensureCursor(); - - // The given screen-space position may have been externally snapped, but the unsnapped position from the input manager - // is used instead since snapping control points doesn't make much sense - cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position; + updateCursor(); break; } } @@ -98,12 +94,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (e.Button != MouseButton.Left) break; - // Find the last non-cursor control point and the respective drawable piece - var lastPoint = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor); - var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == lastPoint); - - if (lastPiece?.IsHovered == true) + if (canPlaceNewControlPoint(out var lastPoint)) { + // Place a new point by detatching the current cursor. + updateCursor(); + cursor = null; + } + else + { + // Transform the last point into a new segment. Debug.Assert(lastPoint != null); segmentStart = lastPoint; @@ -111,11 +110,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders currentSegmentLength = 1; } - else - { - ensureCursor(); - cursor = null; // Detatch the cursor - } return true; } @@ -167,17 +161,48 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - private void ensureCursor() + private void updateCursor() { - if (cursor == null) + if (canPlaceNewControlPoint(out _)) { - HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } }); - currentSegmentLength++; + // The cursor does not overlap a previous control point, so it can be added if not already existing. + if (cursor == null) + { + HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } }); - updatePathType(); + // The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier). + currentSegmentLength++; + updatePathType(); + } - Logger.Log("Set cursor"); + // Update the cursor position. + cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position; } + else if (cursor != null) + { + // The cursor overlaps a previous control point, so it's removed. + HitObject.Path.ControlPoints.Remove(cursor); + cursor = null; + + // The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear). + currentSegmentLength--; + updatePathType(); + } + } + + /// + /// Whether a new control point can be placed at the current mouse position. + /// + /// The last-placed control point. May be null, but is not null if false is returned. + /// Whether a new control point can be placed at the current position. + private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint) + { + // We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point. + var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor); + var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last); + + lastPoint = last; + return lastPiece?.IsHovered != true; } private void updateSlider() From bde0b259c1c03d2022124c6125dd2cc45a292807 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 15:31:54 +0900 Subject: [PATCH 61/77] Improve slider placement test scene --- .../TestSceneSliderPlacementBlueprint.cs | 267 ++++++++++++++++++ .../Visual/PlacementBlueprintTestScene.cs | 28 +- 2 files changed, 282 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs index 0522260150..9fc479953e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs @@ -1,18 +1,285 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene { + [SetUp] + public void Setup() => Schedule(() => + { + HitObjectContainer.Clear(); + ResetPlacement(); + }); + + [Test] + public void TestBeginPlacementWithoutFinishing() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + assertPlaced(false); + } + + [Test] + public void TestPlaceWithoutMovingMouse() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertLength(0); + assertControlPointType(0, PathType.Linear); + } + + [Test] + public void TestPlaceWithMouseMovement() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 200)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertLength(200); + assertControlPointCount(2); + assertControlPointType(0, PathType.Linear); + } + + [Test] + public void TestPlaceNormalControlPoint() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestPlaceTwoNormalControlPoints() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(4); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointPosition(2, new Vector2(100, 100)); + assertControlPointType(0, PathType.Bezier); + } + + [Test] + public void TestPlaceSegmentControlPoint() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointType(0, PathType.Linear); + assertControlPointType(1, PathType.Linear); + } + + [Test] + public void TestMoveToPerfectCurveThenPlaceLinear() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(2); + assertControlPointType(0, PathType.Linear); + assertLength(100); + } + + [Test] + public void TestMoveToBezierThenPlacePerfectCurve() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestMoveToFourthOrderBezierThenPlaceThirdOrderBezier() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400)); + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(4); + assertControlPointType(0, PathType.Bezier); + } + + [Test] + public void TestPlaceLinearSegmentThenPlaceLinearSegment() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointPosition(2, new Vector2(100)); + assertControlPointType(0, PathType.Linear); + assertControlPointType(1, PathType.Linear); + } + + [Test] + public void TestPlaceLinearSegmentThenPlacePerfectCurveSegment() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(4); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointPosition(2, new Vector2(100)); + assertControlPointType(0, PathType.Linear); + assertControlPointType(1, PathType.PerfectCurve); + } + + [Test] + public void TestPlacePerfectCurveSegmentThenPlacePerfectCurveSegment() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 300)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(5); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointPosition(2, new Vector2(100)); + assertControlPointPosition(3, new Vector2(200, 100)); + assertControlPointPosition(4, new Vector2(200)); + assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(2, PathType.PerfectCurve); + } + + private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); + + private void addClickStep(MouseButton button) + { + AddStep($"press {button}", () => InputManager.PressButton(button)); + AddStep($"release {button}", () => InputManager.ReleaseButton(button)); + } + + private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected); + + private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1)); + + private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected); + + private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type.Value == type); + + private void assertControlPointPosition(int index, Vector2 position) => + AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position.Value, 1)); + + private Slider getSlider() => HitObjectContainer.Count > 0 ? (Slider)((DrawableSlider)HitObjectContainer[0]).HitObject : null; + protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject); protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); } diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index ce95dfa62f..dc67d28f63 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -4,8 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Input.Events; using osu.Framework.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -15,13 +13,11 @@ using osu.Game.Screens.Edit.Compose; namespace osu.Game.Tests.Visual { [Cached(Type = typeof(IPlacementHandler))] - public abstract class PlacementBlueprintTestScene : OsuTestScene, IPlacementHandler + public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler { - protected Container HitObjectContainer; + protected readonly Container HitObjectContainer; private PlacementBlueprint currentBlueprint; - private InputManager inputManager; - protected PlacementBlueprintTestScene() { Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); @@ -45,8 +41,7 @@ namespace osu.Game.Tests.Visual { base.LoadComplete(); - inputManager = GetContainingInputManager(); - Add(currentBlueprint = CreateBlueprint()); + ResetPlacement(); } public void BeginPlacement(HitObject hitObject) @@ -58,7 +53,13 @@ namespace osu.Game.Tests.Visual if (commit) AddHitObject(CreateHitObject(hitObject)); - Remove(currentBlueprint); + ResetPlacement(); + } + + protected void ResetPlacement() + { + if (currentBlueprint != null) + Remove(currentBlueprint); Add(currentBlueprint = CreateBlueprint()); } @@ -66,10 +67,11 @@ namespace osu.Game.Tests.Visual { } - protected override bool OnMouseMove(MouseMoveEvent e) + protected override void Update() { - currentBlueprint.UpdatePosition(e.ScreenSpaceMousePosition); - return true; + base.Update(); + + currentBlueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position); } public override void Add(Drawable drawable) @@ -79,7 +81,7 @@ namespace osu.Game.Tests.Visual if (drawable is PlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdatePosition(inputManager.CurrentState.Mouse.Position); + blueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position); } } From 0eaff00787293a93f945bdf0f5866b5285e2626f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 09:45:49 +0300 Subject: [PATCH 62/77] Fix typo in test --- .../Visual/UserInterface/TestSceneOverlayScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index fd3b6ed3ab..3ef0adcd9d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("scroll to start", () => scroll.ScrollToStart(false)); AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); - AddStep("scroll to 250", () => scroll.ScrollTo(500)); + AddStep("scroll to 500", () => scroll.ScrollTo(500)); AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); } From bdce79ed5b85b01de2f49e24e22aa8156a518f3c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 09:57:05 +0300 Subject: [PATCH 63/77] Fix incorrect test step name --- .../Visual/UserInterface/TestSceneOverlayScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 3ef0adcd9d..6a09fecc0a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); AddStep("scroll to 500", () => scroll.ScrollTo(500)); - AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); + AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); } From 9a65aa18d78407bbba9658e0fc98810fc2940c69 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 16:13:14 +0900 Subject: [PATCH 64/77] Fix connections hidden due to overlapping controlpoints --- .../TestScenePathControlPointVisualiser.cs | 64 +++++++++++++++++++ .../PathControlPointConnectionPiece.cs | 16 +++-- .../Components/PathControlPointVisualiser.cs | 57 +++++++++-------- 3 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs new file mode 100644 index 0000000000..cbe14ff4d2 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestScenePathControlPointVisualiser : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(StringHumanizeExtensions), + typeof(PathControlPointPiece), + typeof(PathControlPointConnectionPiece) + }; + + private Slider slider; + private PathControlPointVisualiser visualiser; + + [SetUp] + public void Setup() => Schedule(() => + { + slider = new Slider(); + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + }); + + [Test] + public void TestAddOverlappingControlPoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200)); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + + AddAssert("last connection displayed", () => + { + var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position.Value == new Vector2(300)); + return lastConnection.DrawWidth > 50; + }); + } + + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position))); + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 0fc441fec6..9c620ecb2f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -16,22 +16,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// public class PathControlPointConnectionPiece : CompositeDrawable { - public PathControlPoint ControlPoint; + public readonly PathControlPoint ControlPoint; private readonly Path path; private readonly Slider slider; + private readonly int controlPointIndex; private IBindable sliderPosition; private IBindable pathVersion; - public PathControlPointConnectionPiece(Slider slider, PathControlPoint controlPoint) + public PathControlPointConnectionPiece(Slider slider, int controlPointIndex) { this.slider = slider; - ControlPoint = controlPoint; + this.controlPointIndex = controlPointIndex; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; + ControlPoint = slider.Path.ControlPoints[controlPointIndex]; + InternalChild = path = new SmoothPath { Anchor = Anchor.Centre, @@ -61,13 +64,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components path.ClearVertices(); - int index = slider.Path.ControlPoints.IndexOf(ControlPoint) + 1; - - if (index == 0 || index == slider.Path.ControlPoints.Count) + int nextIndex = controlPointIndex + 1; + if (nextIndex == 0 || nextIndex == slider.Path.ControlPoints.Count) return; path.AddVertex(Vector2.Zero); - path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value); + path.AddVertex(slider.Path.ControlPoints[nextIndex].Position.Value - ControlPoint.Position.Value); path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index e293eba9d7..f6354bc612 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using Humanizer; using osu.Framework.Bindables; @@ -24,17 +25,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { internal readonly Container Pieces; + internal readonly Container Connections; - private readonly Container connections; - + private readonly IBindableList controlPoints = new BindableList(); private readonly Slider slider; - private readonly bool allowSelection; private InputManager inputManager; - private IBindableList controlPoints; - public Action> RemoveControlPointsRequested; public PathControlPointVisualiser(Slider slider, bool allowSelection) @@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components InternalChildren = new Drawable[] { - connections = new Container { RelativeSizeAxes = Axes.Both }, + Connections = new Container { RelativeSizeAxes = Axes.Both }, Pieces = new Container { RelativeSizeAxes = Axes.Both } }; } @@ -57,33 +55,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components inputManager = GetContainingInputManager(); - controlPoints = slider.Path.ControlPoints.GetBoundCopy(); - controlPoints.ItemsAdded += addControlPoints; - controlPoints.ItemsRemoved += removeControlPoints; - - addControlPoints(controlPoints); + controlPoints.CollectionChanged += onControlPointsChanged; + controlPoints.BindTo(slider.Path.ControlPoints); } - private void addControlPoints(IEnumerable controlPoints) + private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) { - foreach (var point in controlPoints) + switch (e.Action) { - Pieces.Add(new PathControlPointPiece(slider, point).With(d => - { - if (allowSelection) - d.RequestSelection = selectPiece; - })); + case NotifyCollectionChangedAction.Add: + for (int i = 0; i < e.NewItems.Count; i++) + { + var point = (PathControlPoint)e.NewItems[i]; - connections.Add(new PathControlPointConnectionPiece(slider, point)); - } - } + Pieces.Add(new PathControlPointPiece(slider, point).With(d => + { + if (allowSelection) + d.RequestSelection = selectPiece; + })); - private void removeControlPoints(IEnumerable controlPoints) - { - foreach (var point in controlPoints) - { - Pieces.RemoveAll(p => p.ControlPoint == point); - connections.RemoveAll(c => c.ControlPoint == point); + Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i)); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var point in e.OldItems.Cast()) + { + Pieces.RemoveAll(p => p.ControlPoint == point); + Connections.RemoveAll(c => c.ControlPoint == point); + } + + break; } } From 29dd2252054ed45fbac8be5f43318bdae45552f5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 10:45:15 +0300 Subject: [PATCH 65/77] Make button protected --- .../UserInterface/TestSceneOverlayScrollContainer.cs | 9 +++++++-- osu.Game/Overlays/OverlayScrollContainer.cs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 6a09fecc0a..e9e63613c0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -25,14 +25,14 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private OverlayScrollContainer scroll; + private TestScrollContainer scroll; private int invocationCount; [SetUp] public void SetUp() => Schedule(() => { - Child = scroll = new OverlayScrollContainer + Child = scroll = new TestScrollContainer { RelativeSizeAxes = Axes.Both, Child = new Container @@ -104,5 +104,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("invocation count is 1", () => invocationCount == 1); } + + private class TestScrollContainer : OverlayScrollContainer + { + public new ScrollToTopButton Button => base.Button; + } } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index a8f2a0ce6c..9af09f0f6a 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays /// private const int button_scroll_position = 200; - public ScrollToTopButton Button { get; } + protected readonly ScrollToTopButton Button; private float currentTarget; From b8ecc41667301cccff0d3e81bccdc95f6946a69f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 10:52:34 +0300 Subject: [PATCH 66/77] Add comment --- osu.Game/Overlays/OverlayScrollContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 9af09f0f6a..e95a6379f1 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -56,6 +56,7 @@ namespace osu.Game.Overlays return; } + // Clicking on button should immediately cause it's disappearance, so we don't want to override it's state until we have a new target. if (Target == currentTarget) return; From 1e3251e3e936f0ce0eca7a77dd7ae5dbffab27eb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 10:59:53 +0300 Subject: [PATCH 67/77] Remove excessive logic --- osu.Game/Overlays/OverlayScrollContainer.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index e95a6379f1..e7415e6f74 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -28,8 +28,6 @@ namespace osu.Game.Overlays protected readonly ScrollToTopButton Button; - private float currentTarget; - public OverlayScrollContainer() { AddInternal(Button = new ScrollToTopButton @@ -40,7 +38,6 @@ namespace osu.Game.Overlays Action = () => { ScrollToStart(); - currentTarget = Target; Button.State = Visibility.Hidden; } }); @@ -56,11 +53,6 @@ namespace osu.Game.Overlays return; } - // Clicking on button should immediately cause it's disappearance, so we don't want to override it's state until we have a new target. - if (Target == currentTarget) - return; - - currentTarget = Target; Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } From bb53f96c717e9034c5a2e1b2411c3474d076fc9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 17:18:50 +0900 Subject: [PATCH 68/77] Store states as byte[] instead of Streams --- .../Editor/LegacyEditorBeatmapDifferTest.cs | 19 +++++++-------- osu.Game/Screens/Edit/EditorChangeHandler.cs | 18 ++++++++------ .../Screens/Edit/LegacyEditorBeatmapDiffer.cs | 24 +++++++------------ 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs index d70a112b7f..ecd3799cb1 100644 --- a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs +++ b/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs @@ -315,26 +315,25 @@ namespace osu.Game.Tests.Editor differ.Patch(encode(current), encode(patch)); // Convert beatmaps to strings for assertion purposes. - string currentStr = Encoding.ASCII.GetString(encode(current).ToArray()); - string patchStr = Encoding.ASCII.GetString(encode(patch).ToArray()); + string currentStr = Encoding.ASCII.GetString(encode(current)); + string patchStr = Encoding.ASCII.GetString(encode(patch)); Assert.That(currentStr, Is.EqualTo(patchStr)); } - private MemoryStream encode(IBeatmap beatmap) + private byte[] encode(IBeatmap beatmap) { - var encoded = new MemoryStream(); - + using (var encoded = new MemoryStream()) using (var sw = new StreamWriter(encoded, leaveOpen: true)) + { new LegacyBeatmapEncoder(beatmap).Encode(sw); - - return encoded; + return encoded.ToArray(); + } } - private IBeatmap decode(Stream stream) + private IBeatmap decode(byte[] state) { - stream.Seek(0, SeekOrigin.Begin); - + using (var stream = new MemoryStream(state)) using (var reader = new LineBufferedReader(stream, true)) return Decoder.GetDecoder(reader).Decode(reader); } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 7e372926ba..22f076d939 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -16,7 +16,9 @@ namespace osu.Game.Screens.Edit public class EditorChangeHandler : IEditorChangeHandler { private readonly LegacyEditorBeatmapDiffer differ; - private readonly List savedStates = new List(); + + private readonly List savedStates = new List(); + private int currentState = -1; private readonly EditorBeatmap editorBeatmap; @@ -69,15 +71,17 @@ namespace osu.Game.Screens.Edit if (isRestoring) return; - var stream = new MemoryStream(); - - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); - if (currentState < savedStates.Count - 1) savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); - savedStates.Add(stream); + using (var stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + + savedStates.Add(stream.ToArray()); + } + currentState = savedStates.Count - 1; } diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs index 8d2f577a1d..c62f21bb39 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs @@ -4,12 +4,13 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using DiffPlex; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; using osu.Game.IO; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Screens.Edit { @@ -22,7 +23,7 @@ namespace osu.Game.Screens.Edit this.editorBeatmap = editorBeatmap; } - public void Patch(Stream currentState, Stream newState) + public void Patch(byte[] currentState, byte[] newState) { // Diff the beatmaps var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false); @@ -36,7 +37,7 @@ namespace osu.Game.Screens.Edit foreach (var block in result.DiffBlocks) { - // Removed hitobject + // Removed hitobjects for (int i = 0; i < block.DeleteCountA; i++) { int hoIndex = block.DeleteStartA + i - oldHitObjectsIndex - 1; @@ -47,7 +48,7 @@ namespace osu.Game.Screens.Edit toRemove.Add(hoIndex); } - // Added hitobject + // Added hitobjects for (int i = 0; i < block.InsertCountB; i++) { int hoIndex = block.InsertStartB + i - newHitObjectsIndex - 1; @@ -74,18 +75,11 @@ namespace osu.Game.Screens.Edit } } - private string readString(Stream stream) + private string readString(byte[] state) => Encoding.UTF8.GetString(state); + + private IBeatmap readBeatmap(byte[] state) { - stream.Seek(0, SeekOrigin.Begin); - - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8, true, 1024, true)) - return sr.ReadToEnd(); - } - - private IBeatmap readBeatmap(Stream stream) - { - stream.Seek(0, SeekOrigin.Begin); - + using (var stream = new MemoryStream(state)) using (var reader = new LineBufferedReader(stream, true)) return new PassThroughWorkingBeatmap(Decoder.GetDecoder(reader).Decode(reader)).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset); } From 6aab19413c793bce5650fa1aa6a95a82a3048e4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 17:20:01 +0900 Subject: [PATCH 69/77] Rename differ to patcher, add xmldoc --- ...mapDifferTest.cs => LegacyEditorBeatmapPatcherTest.cs} | 8 ++++---- osu.Game/Screens/Edit/EditorChangeHandler.cs | 6 +++--- ...itorBeatmapDiffer.cs => LegacyEditorBeatmapPatcher.cs} | 7 +++++-- 3 files changed, 12 insertions(+), 9 deletions(-) rename osu.Game.Tests/Editor/{LegacyEditorBeatmapDifferTest.cs => LegacyEditorBeatmapPatcherTest.cs} (97%) rename osu.Game/Screens/Edit/{LegacyEditorBeatmapDiffer.cs => LegacyEditorBeatmapPatcher.cs} (93%) diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs similarity index 97% rename from osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs rename to osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs index ecd3799cb1..11c6399d19 100644 --- a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs +++ b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs @@ -20,15 +20,15 @@ using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Tests.Editor { [TestFixture] - public class LegacyEditorBeatmapDifferTest + public class LegacyEditorBeatmapPatcherTest { - private LegacyEditorBeatmapDiffer differ; + private LegacyEditorBeatmapPatcher patcher; private EditorBeatmap current; [SetUp] public void Setup() { - differ = new LegacyEditorBeatmapDiffer(current = new EditorBeatmap(new OsuBeatmap + patcher = new LegacyEditorBeatmapPatcher(current = new EditorBeatmap(new OsuBeatmap { BeatmapInfo = { @@ -312,7 +312,7 @@ namespace osu.Game.Tests.Editor patch = decode(encode(patch)); // Apply the patch. - differ.Patch(encode(current), encode(patch)); + patcher.Patch(encode(current), encode(patch)); // Convert beatmaps to strings for assertion purposes. string currentStr = Encoding.ASCII.GetString(encode(current)); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 22f076d939..00a27801f4 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit /// public class EditorChangeHandler : IEditorChangeHandler { - private readonly LegacyEditorBeatmapDiffer differ; + private readonly LegacyEditorBeatmapPatcher patcher; private readonly List savedStates = new List(); @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit editorBeatmap.HitObjectRemoved += hitObjectRemoved; editorBeatmap.HitObjectUpdated += hitObjectUpdated; - differ = new LegacyEditorBeatmapDiffer(editorBeatmap); + patcher = new LegacyEditorBeatmapPatcher(editorBeatmap); // Initial state. SaveState(); @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Edit isRestoring = true; - differ.Patch(savedStates[currentState], savedStates[newState]); + patcher.Patch(savedStates[currentState], savedStates[newState]); currentState = newState; isRestoring = false; diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs similarity index 93% rename from osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs rename to osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index c62f21bb39..17eba87076 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -14,11 +14,14 @@ using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Screens.Edit { - public class LegacyEditorBeatmapDiffer + /// + /// Patches an based on the difference between two legacy (.osu) states. + /// + public class LegacyEditorBeatmapPatcher { private readonly EditorBeatmap editorBeatmap; - public LegacyEditorBeatmapDiffer(EditorBeatmap editorBeatmap) + public LegacyEditorBeatmapPatcher(EditorBeatmap editorBeatmap) { this.editorBeatmap = editorBeatmap; } From dd949a3fe09ebaa45143be64075efe89aafc08f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 17:52:04 +0900 Subject: [PATCH 70/77] Fix test writer flush happening too late --- osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs index 11c6399d19..c24418d688 100644 --- a/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs @@ -324,9 +324,10 @@ namespace osu.Game.Tests.Editor private byte[] encode(IBeatmap beatmap) { using (var encoded = new MemoryStream()) - using (var sw = new StreamWriter(encoded, leaveOpen: true)) { - new LegacyBeatmapEncoder(beatmap).Encode(sw); + using (var sw = new StreamWriter(encoded)) + new LegacyBeatmapEncoder(beatmap).Encode(sw); + return encoded.ToArray(); } } @@ -334,7 +335,7 @@ namespace osu.Game.Tests.Editor private IBeatmap decode(byte[] state) { using (var stream = new MemoryStream(state)) - using (var reader = new LineBufferedReader(stream, true)) + using (var reader = new LineBufferedReader(stream)) return Decoder.GetDecoder(reader).Decode(reader); } } From 409cda3cc0d5a8edeb8470a88648499bab9d511d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2020 08:53:21 +0000 Subject: [PATCH 71/77] Bump BenchmarkDotNet from 0.12.0 to 0.12.1 Bumps [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) from 0.12.0 to 0.12.1. - [Release notes](https://github.com/dotnet/BenchmarkDotNet/releases) - [Commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.12.0...v0.12.1) Signed-off-by: dependabot-preview[bot] --- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index f2e1c0ec3b..88fe8f1150 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -7,7 +7,7 @@ - + From b741e359cd5abf64d3428c3ff0da0e2ae1f1e436 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 12:23:28 +0300 Subject: [PATCH 72/77] Use OverlayScrollContainer for overlays --- .../Graphics/Containers/SectionsContainer.cs | 27 ++++++++++++------- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- osu.Game/Overlays/BeatmapSetOverlay.cs | 4 +-- osu.Game/Overlays/ChangelogOverlay.cs | 2 +- osu.Game/Overlays/NewsOverlay.cs | 2 +- osu.Game/Overlays/RankingsOverlay.cs | 4 +-- .../SearchableList/SearchableListOverlay.cs | 2 +- osu.Game/Overlays/UserProfileOverlay.cs | 2 ++ 8 files changed, 27 insertions(+), 18 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 07a50c39e1..24c61ad11c 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,7 +20,7 @@ namespace osu.Game.Graphics.Containers private Drawable expandableHeader, fixedHeader, footer, headerBackground; private readonly OsuScrollContainer scrollContainer; private readonly Container headerBackgroundContainer; - private readonly FlowContainer scrollContentContainer; + private FlowContainer scrollContentContainer; protected override Container Content => scrollContentContainer; @@ -125,20 +126,26 @@ namespace osu.Game.Graphics.Containers public SectionsContainer() { - AddInternal(scrollContainer = new OsuScrollContainer + AddRangeInternal(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - ScrollbarVisible = false, - Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() } - }); - AddInternal(headerBackgroundContainer = new Container - { - RelativeSizeAxes = Axes.X + scrollContainer = CreateScrollContainer().With(s => + { + s.RelativeSizeAxes = Axes.Both; + s.Masking = true; + s.ScrollbarVisible = false; + s.Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() }; + }), + headerBackgroundContainer = new Container + { + RelativeSizeAxes = Axes.X + } }); originalSectionsMargin = scrollContentContainer.Margin; } + [NotNull] + protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); public void ScrollToTop() => scrollContainer.ScrollTo(0); diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5bac5a5402..b450f33ee1 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background6 }, - new BasicScrollContainer + new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 0d16c4842d..3e23442023 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays public BeatmapSetOverlay() : base(OverlayColourScheme.Blue) { - OsuScrollContainer scroll; + OverlayScrollContainer scroll; Info info; CommentsSection comments; @@ -49,7 +49,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, - scroll = new OsuScrollContainer + scroll = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index d13ac5c2de..726be9e194 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background4, }, - new OsuScrollContainer + new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 71c205ff63..6c9477cbc4 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = colours.PurpleDarkAlternative }, - new OsuScrollContainer + new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, Child = new FillFlowContainer diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index afb23883ac..7b200d4226 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays protected Bindable Scope => header.Current; - private readonly BasicScrollContainer scrollFlow; + private readonly OverlayScrollContainer scrollFlow; private readonly Container contentContainer; private readonly LoadingLayer loading; private readonly Box background; @@ -44,7 +44,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, - scrollFlow = new BasicScrollContainer + scrollFlow = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index d6174e0733..ebd12913f5 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays.SearchableList { RelativeSizeAxes = Axes.Both, Masking = true, - Child = new OsuScrollContainer + Child = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 6ec30f7707..b4c8a2d3ca 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -195,6 +195,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both; } + protected override OsuScrollContainer CreateScrollContainer() => new OverlayScrollContainer(); + protected override FlowContainer CreateScrollContentContainer() => new FillFlowContainer { Direction = FillDirection.Vertical, From 4c5d01a611ddc88cd847a2d91db27ff57bc3e037 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 12:34:51 +0300 Subject: [PATCH 73/77] Remove unused usings --- osu.Game/Overlays/NewsOverlay.cs | 1 - osu.Game/Overlays/SearchableList/SearchableListOverlay.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 6c9477cbc4..46d692d44d 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Overlays.News; namespace osu.Game.Overlays diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index ebd12913f5..4ab2de06b6 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; namespace osu.Game.Overlays.SearchableList From 0be2dc9b2dee3eecb9fdac94a9f8e64745230d72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 20:12:51 +0900 Subject: [PATCH 74/77] Tidy up SectionsContainer class layout/ordering --- .../Graphics/Containers/SectionsContainer.cs | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 24c61ad11c..a3125614aa 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -17,12 +17,7 @@ namespace osu.Game.Graphics.Containers public class SectionsContainer : Container where T : Drawable { - private Drawable expandableHeader, fixedHeader, footer, headerBackground; - private readonly OsuScrollContainer scrollContainer; - private readonly Container headerBackgroundContainer; - private FlowContainer scrollContentContainer; - - protected override Container Content => scrollContentContainer; + public Bindable SelectedSection { get; } = new Bindable(); public Drawable ExpandableHeader { @@ -84,6 +79,7 @@ namespace osu.Game.Graphics.Containers headerBackgroundContainer.Clear(); headerBackground = value; + if (value == null) return; headerBackgroundContainer.Add(headerBackground); @@ -92,37 +88,17 @@ namespace osu.Game.Graphics.Containers } } - public Bindable SelectedSection { get; } = new Bindable(); + protected override Container Content => scrollContentContainer; - protected virtual FlowContainer CreateScrollContentContainer() - => new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }; - - public override void Add(T drawable) - { - base.Add(drawable); - lastKnownScroll = float.NaN; - headerHeight = float.NaN; - footerHeight = float.NaN; - } + private readonly OsuScrollContainer scrollContainer; + private readonly Container headerBackgroundContainer; + private readonly MarginPadding originalSectionsMargin; + private Drawable expandableHeader, fixedHeader, footer, headerBackground; + private FlowContainer scrollContentContainer; private float headerHeight, footerHeight; - private readonly MarginPadding originalSectionsMargin; - private void updateSectionsMargin() - { - if (!Children.Any()) return; - - var newMargin = originalSectionsMargin; - newMargin.Top += headerHeight; - newMargin.Bottom += footerHeight; - - scrollContentContainer.Margin = newMargin; - } + private float lastKnownScroll; public SectionsContainer() { @@ -133,22 +109,41 @@ namespace osu.Game.Graphics.Containers s.RelativeSizeAxes = Axes.Both; s.Masking = true; s.ScrollbarVisible = false; - s.Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() }; + s.Child = scrollContentContainer = CreateScrollContentContainer(); }), headerBackgroundContainer = new Container { RelativeSizeAxes = Axes.X } }); + originalSectionsMargin = scrollContentContainer.Margin; } + public override void Add(T drawable) + { + base.Add(drawable); + lastKnownScroll = float.NaN; + headerHeight = float.NaN; + footerHeight = float.NaN; + } + + public void ScrollTo(Drawable section) => + scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); + + public void ScrollToTop() => scrollContainer.ScrollTo(0); + [NotNull] protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); - public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); - - public void ScrollToTop() => scrollContainer.ScrollTo(0); + [NotNull] + protected virtual FlowContainer CreateScrollContentContainer() => + new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { @@ -163,8 +158,6 @@ namespace osu.Game.Graphics.Containers return result; } - private float lastKnownScroll; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -215,5 +208,16 @@ namespace osu.Game.Graphics.Containers SelectedSection.Value = bestMatch; } } + + private void updateSectionsMargin() + { + if (!Children.Any()) return; + + var newMargin = originalSectionsMargin; + newMargin.Top += headerHeight; + newMargin.Bottom += footerHeight; + + scrollContentContainer.Margin = newMargin; + } } } From 2388799acfb8a781894d2c41de06c7f25b92bccb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 20:34:18 +0900 Subject: [PATCH 75/77] Limit upper number of editor beatmap states saved to 50 --- .../Editor/EditorChangeHandlerTest.cs | 71 +++++++++++++++++++ osu.Game/Screens/Edit/EditorChangeHandler.cs | 7 ++ 2 files changed, 78 insertions(+) create mode 100644 osu.Game.Tests/Editor/EditorChangeHandlerTest.cs diff --git a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs new file mode 100644 index 0000000000..ef16976130 --- /dev/null +++ b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Screens.Edit; + +namespace osu.Game.Tests.Editor +{ + [TestFixture] + public class EditorChangeHandlerTest + { + [Test] + public void TestSaveRestoreState() + { + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + + Assert.That(handler.HasUndoState, Is.False); + + handler.SaveState(); + + Assert.That(handler.HasUndoState, Is.True); + + handler.RestoreState(-1); + + Assert.That(handler.HasUndoState, Is.False); + } + + [Test] + public void TestMaxStatesSaved() + { + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + + Assert.That(handler.HasUndoState, Is.False); + + for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + handler.SaveState(); + + Assert.That(handler.HasUndoState, Is.True); + + for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + { + Assert.That(handler.HasUndoState, Is.True); + handler.RestoreState(-1); + } + + Assert.That(handler.HasUndoState, Is.False); + } + + [Test] + public void TestMaxStatesExceeded() + { + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + + Assert.That(handler.HasUndoState, Is.False); + + for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++) + handler.SaveState(); + + Assert.That(handler.HasUndoState, Is.True); + + for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + { + Assert.That(handler.HasUndoState, Is.True); + handler.RestoreState(-1); + } + + Assert.That(handler.HasUndoState, Is.False); + } + } +} diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 00a27801f4..a8204715cd 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit private int bulkChangesStarted; private bool isRestoring; + public const int MAX_SAVED_STATES = 50; + /// /// Creates a new . /// @@ -43,6 +45,8 @@ namespace osu.Game.Screens.Edit SaveState(); } + public bool HasUndoState => currentState > 0; + private void hitObjectAdded(HitObject obj) => SaveState(); private void hitObjectRemoved(HitObject obj) => SaveState(); @@ -74,6 +78,9 @@ namespace osu.Game.Screens.Edit if (currentState < savedStates.Count - 1) savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); + if (savedStates.Count > MAX_SAVED_STATES) + savedStates.RemoveAt(0); + using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) From 4cfc6866835d4d92f2c6f03cd4bfeaa47c845c76 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 21:41:18 +0900 Subject: [PATCH 76/77] Fix excption with 0 control points --- .../Sliders/Components/PathControlPointConnectionPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 9c620ecb2f..ba1d35c35c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components path.ClearVertices(); int nextIndex = controlPointIndex + 1; - if (nextIndex == 0 || nextIndex == slider.Path.ControlPoints.Count) + if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count) return; path.AddVertex(Vector2.Zero); From 2b2ab2bf1929d3b6d730d579b73b1bc39e2220e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 21:59:23 +0900 Subject: [PATCH 77/77] Show new segments as red points even when hovered --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 092a13cca5..fed149b5c5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -51,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components this.slider = slider; ControlPoint = controlPoint; + controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); + Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -183,8 +186,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components markerRing.Alpha = IsSelected.Value ? 1 : 0; Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow; + if (IsHovered || IsSelected.Value) - colour = Color4.White; + colour = colour.Lighten(1); + marker.Colour = colour; } }