From 69c1cd5b3430ef536c5c220bd77ed7398c4bab3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 14:17:37 +0200 Subject: [PATCH 1/5] Add failing test case for hit circle animations disable --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs new file mode 100644 index 0000000000..94ee5d7e14 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + [TestFixture] + public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor + { + [Resolved] + private OsuConfigManager config { get; set; } + + [Test] + public void TestHitCircleAnimationDisable() + { + HitCircle hitCircle = null; + + AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0)); + toggleAnimations(true); + seekSmoothlyTo(() => hitCircle.StartTime + 10); + + AddAssert("hit circle piece has transforms", () => + { + var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); + return getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime); + }); + + AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); + toggleAnimations(false); + seekSmoothlyTo(() => hitCircle.StartTime + 10); + + AddAssert("hit circle piece has no transforms", () => + { + var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); + return getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime); + }); + } + + private HitCircle getHitCircle(int index) + => EditorBeatmap.HitObjects.OfType().ElementAt(index); + + private DrawableHitObject getDrawableObjectFor(HitObject hitObject) + => this.ChildrenOfType().Single(ho => ho.HitObject == hitObject); + + private IEnumerable getTransformsRecursively(Drawable drawable) + => drawable.ChildrenOfType().SelectMany(d => d.Transforms); + + private void toggleAnimations(bool enabled) + => AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled)); + + private void seekSmoothlyTo(Func targetTime) + { + AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke())); + AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime)); + } + } +} From e2a370f60244a0ccac1450ed604a79a2be468a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 14:57:04 +0200 Subject: [PATCH 2/5] Add coverage for hit circle fade-out duration --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 19 +++++++++++-------- .../Edit/DrawableOsuEditorRuleset.cs | 16 ++++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs index 94ee5d7e14..18de3a8414 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -13,6 +13,7 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -28,25 +29,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public void TestHitCircleAnimationDisable() { HitCircle hitCircle = null; + DrawableHitCircle drawableHitCircle = null; AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0)); toggleAnimations(true); seekSmoothlyTo(() => hitCircle.StartTime + 10); - AddAssert("hit circle piece has transforms", () => - { - var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); - return getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime); - }); + AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); + AddAssert("hit circle piece has transforms", + () => getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime)); AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); toggleAnimations(false); seekSmoothlyTo(() => hitCircle.StartTime + 10); - AddAssert("hit circle piece has no transforms", () => + AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); + AddAssert("hit circle piece has no transforms", + () => getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime)); + AddAssert("hit circle has longer fade-out applied", () => { - var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); - return getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime); + var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha)); + return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION; }); } diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index aeeae84d14..9143b154b9 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -20,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Edit { public class DrawableOsuEditorRuleset : DrawableOsuRuleset { + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// + public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700; + public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) { @@ -46,12 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit d.ApplyCustomUpdateState += updateState; } - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - private const double editor_hit_object_fade_out_extension = 700; - private void updateState(DrawableHitObject hitObject, ArmedState state) { if (state == ArmedState.Idle || hitAnimations.Value) @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObject is DrawableHitCircle circle) { circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension * 4) + .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4) .Expire(); circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Edit hitObject.RemoveTransform(existing); using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire(); break; } } From fe48ddfee3242cde208b64cb7a59967f0d670885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:26:31 +0200 Subject: [PATCH 3/5] Also cover slider animation disable --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs index 18de3a8414..7ffa2c1f94 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -36,16 +36,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor seekSmoothlyTo(() => hitCircle.StartTime + 10); AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); - AddAssert("hit circle piece has transforms", - () => getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime)); + assertFutureTransforms(() => drawableHitCircle.CirclePiece, true); AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); toggleAnimations(false); seekSmoothlyTo(() => hitCircle.StartTime + 10); AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); - AddAssert("hit circle piece has no transforms", - () => getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime)); + assertFutureTransforms(() => drawableHitCircle.CirclePiece, false); AddAssert("hit circle has longer fade-out applied", () => { var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha)); @@ -53,9 +51,47 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + [Test] + public void TestSliderAnimationDisable() + { + Slider slider = null; + DrawableSlider drawableSlider = null; + DrawableSliderRepeat sliderRepeat = null; + + AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0)); + toggleAnimations(true); + seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10); + + retrieveDrawables(); + assertFutureTransforms(() => sliderRepeat, true); + + AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1)); + toggleAnimations(false); + seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10); + + retrieveDrawables(); + assertFutureTransforms(() => sliderRepeat.Arrow, false); + seekSmoothlyTo(() => slider.GetEndTime()); + AddAssert("slider has longer fade-out applied", () => + { + var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha)); + return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION; + }); + + void retrieveDrawables() => + AddStep("retrieve drawables", () => + { + drawableSlider = (DrawableSlider)getDrawableObjectFor(slider); + sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType().First()); + }); + } + private HitCircle getHitCircle(int index) => EditorBeatmap.HitObjects.OfType().ElementAt(index); + private Slider getSliderWithRepeats(int index) + => EditorBeatmap.HitObjects.OfType().Where(s => s.RepeatCount >= 1).ElementAt(index); + private DrawableHitObject getDrawableObjectFor(HitObject hitObject) => this.ChildrenOfType().Single(ho => ho.HitObject == hitObject); @@ -70,5 +106,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke())); AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime)); } + + private void assertFutureTransforms(Func getDrawable, bool hasFutureTransforms) + => AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms", + () => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms); } } From e94fbd83e2690c5c34c8ccd0cebdacaae636f4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:30:45 +0200 Subject: [PATCH 4/5] Ensure editor ruleset animation disable execution order --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 9143b154b9..54fd95a618 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -69,8 +69,14 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObject is IHasMainCirclePiece mainPieceContainer) { // clear any explode animation logic. - mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + // this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables. + ScheduleAfterChildren(() => + { + if (hitObject.HitObject == null) return; + + mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); + mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + }); } if (hitObject is DrawableSliderRepeat repeat) From afc89b39d9cb751ab00555c50a055cb607774603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:32:51 +0200 Subject: [PATCH 5/5] Use `StateUpdateTime` for transform clearing logic `MainCirclePiece` specifies a state transform starting at `StateUpdateTime`, which is earlier than the previously-used `HitStateUpdateTime`. Change the transform clearing logic to use the former to ensure that exactly all animation transforms are cleared. --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 54fd95a618..0e61c02e2d 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -74,15 +74,15 @@ namespace osu.Game.Rulesets.Osu.Edit { if (hitObject.HitObject == null) return; - mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true); + mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true); }); } if (hitObject is DrawableSliderRepeat repeat) { - repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true); + repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true); } // adjust the visuals of top-level object types to make them stay on screen for longer than usual.