diff --git a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs index cd25bc1683..8cc7a01acb 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Timing; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit.Screens.Compose; using osu.Game.Tests.Beatmaps; @@ -13,24 +12,15 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { [TestFixture] - public class TestCaseEditorCompose : OsuTestCase + public class TestCaseEditorCompose : EditorClockTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(Compose) }; - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); - [BackgroundDependencyLoader] private void load(OsuGameBase osuGame) { osuGame.Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); - var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - dependencies.CacheAs(clock); - dependencies.CacheAs(clock); - var compose = new Compose(); compose.Beatmap.BindTo(osuGame.Beatmap); diff --git a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs index e9e966a826..62c02ee5aa 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs @@ -3,52 +3,35 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Edit.Screens.Compose; using osu.Game.Tests.Beatmaps; using OpenTK; using OpenTK.Graphics; namespace osu.Game.Tests.Visual { - public class TestCaseEditorSeekSnapping : OsuTestCase + public class TestCaseEditorSeekSnapping : EditorClockTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(HitObjectComposer) }; - private Track track; - private HitObjectComposer composer; - - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(4); - - private DecoupleableInterpolatingFramedClock clock; - - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); + public TestCaseEditorSeekSnapping() + { + BeatDivisor.Value = 4; + } [BackgroundDependencyLoader] private void load(OsuGameBase osuGame) { - clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - dependencies.CacheAs(clock); - dependencies.CacheAs(clock); - dependencies.Cache(beatDivisor); - var testBeatmap = new Beatmap { ControlPointInfo = new ControlPointInfo @@ -71,22 +54,8 @@ namespace osu.Game.Tests.Visual }; osuGame.Beatmap.Value = new TestWorkingBeatmap(testBeatmap); - track = osuGame.Beatmap.Value.Track; - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { composer = new TestHitObjectComposer(new OsuRuleset()) }, - new Drawable[] { new TimingPointVisualiser(testBeatmap, track) { Clock = clock } }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.Distributed), - new Dimension(GridSizeMode.AutoSize), - } - }; + Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock }; testSeekNoSnapping(); testSeekSnappingOnBeat(); @@ -107,18 +76,18 @@ namespace osu.Game.Tests.Visual reset(); // Forwards - AddStep("Seek(0)", () => composer.SeekTo(0)); - AddAssert("Time = 0", () => clock.CurrentTime == 0); - AddStep("Seek(33)", () => composer.SeekTo(33)); - AddAssert("Time = 33", () => clock.CurrentTime == 33); - AddStep("Seek(89)", () => composer.SeekTo(89)); - AddAssert("Time = 89", () => clock.CurrentTime == 89); + AddStep("Seek(0)", () => Clock.Seek(0)); + AddAssert("Time = 0", () => Clock.CurrentTime == 0); + AddStep("Seek(33)", () => Clock.Seek(33)); + AddAssert("Time = 33", () => Clock.CurrentTime == 33); + AddStep("Seek(89)", () => Clock.Seek(89)); + AddAssert("Time = 89", () => Clock.CurrentTime == 89); // Backwards - AddStep("Seek(25)", () => composer.SeekTo(25)); - AddAssert("Time = 25", () => clock.CurrentTime == 25); - AddStep("Seek(0)", () => composer.SeekTo(0)); - AddAssert("Time = 0", () => clock.CurrentTime == 0); + AddStep("Seek(25)", () => Clock.Seek(25)); + AddAssert("Time = 25", () => Clock.CurrentTime == 25); + AddStep("Seek(0)", () => Clock.Seek(0)); + AddAssert("Time = 0", () => Clock.CurrentTime == 0); } /// @@ -129,20 +98,20 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(0), Snap", () => composer.SeekTo(0, true)); - AddAssert("Time = 0", () => clock.CurrentTime == 0); - AddStep("Seek(50), Snap", () => composer.SeekTo(50, true)); - AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("Seek(100), Snap", () => composer.SeekTo(100, true)); - AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("Seek(175), Snap", () => composer.SeekTo(175, true)); - AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("Seek(350), Snap", () => composer.SeekTo(350, true)); - AddAssert("Time = 350", () => clock.CurrentTime == 350); - AddStep("Seek(400), Snap", () => composer.SeekTo(400, true)); - AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("Seek(450), Snap", () => composer.SeekTo(450, true)); - AddAssert("Time = 450", () => clock.CurrentTime == 450); + AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0)); + AddAssert("Time = 0", () => Clock.CurrentTime == 0); + AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50)); + AddAssert("Time = 50", () => Clock.CurrentTime == 50); + AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100)); + AddAssert("Time = 100", () => Clock.CurrentTime == 100); + AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175)); + AddAssert("Time = 175", () => Clock.CurrentTime == 175); + AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350)); + AddAssert("Time = 350", () => Clock.CurrentTime == 350); + AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400)); + AddAssert("Time = 400", () => Clock.CurrentTime == 400); + AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450)); + AddAssert("Time = 450", () => Clock.CurrentTime == 450); } /// @@ -154,18 +123,18 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(24), Snap", () => composer.SeekTo(24, true)); - AddAssert("Time = 0", () => clock.CurrentTime == 0); - AddStep("Seek(26), Snap", () => composer.SeekTo(26, true)); - AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("Seek(150), Snap", () => composer.SeekTo(150, true)); - AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("Seek(170), Snap", () => composer.SeekTo(170, true)); - AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("Seek(274), Snap", () => composer.SeekTo(274, true)); - AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("Seek(276), Snap", () => composer.SeekTo(276, true)); - AddAssert("Time = 350", () => clock.CurrentTime == 350); + AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24)); + AddAssert("Time = 0", () => Clock.CurrentTime == 0); + AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26)); + AddAssert("Time = 50", () => Clock.CurrentTime == 50); + AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150)); + AddAssert("Time = 100", () => Clock.CurrentTime == 100); + AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170)); + AddAssert("Time = 175", () => Clock.CurrentTime == 175); + AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274)); + AddAssert("Time = 175", () => Clock.CurrentTime == 175); + AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276)); + AddAssert("Time = 350", () => Clock.CurrentTime == 350); } /// @@ -175,16 +144,16 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("SeekForward", () => composer.SeekForward()); - AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("SeekForward", () => composer.SeekForward()); - AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("SeekForward", () => composer.SeekForward()); - AddAssert("Time = 200", () => clock.CurrentTime == 200); - AddStep("SeekForward", () => composer.SeekForward()); - AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("SeekForward", () => composer.SeekForward()); - AddAssert("Time = 450", () => clock.CurrentTime == 450); + AddStep("SeekForward", () => Clock.SeekForward()); + AddAssert("Time = 50", () => Clock.CurrentTime == 50); + AddStep("SeekForward", () => Clock.SeekForward()); + AddAssert("Time = 100", () => Clock.CurrentTime == 100); + AddStep("SeekForward", () => Clock.SeekForward()); + AddAssert("Time = 200", () => Clock.CurrentTime == 200); + AddStep("SeekForward", () => Clock.SeekForward()); + AddAssert("Time = 400", () => Clock.CurrentTime == 400); + AddStep("SeekForward", () => Clock.SeekForward()); + AddAssert("Time = 450", () => Clock.CurrentTime == 450); } /// @@ -194,18 +163,18 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 350", () => clock.CurrentTime == 350); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 450", () => clock.CurrentTime == 450); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 50", () => Clock.CurrentTime == 50); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 100", () => Clock.CurrentTime == 100); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 175", () => Clock.CurrentTime == 175); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 350", () => Clock.CurrentTime == 350); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 400", () => Clock.CurrentTime == 400); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 450", () => Clock.CurrentTime == 450); } /// @@ -216,30 +185,30 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(49)", () => composer.SeekTo(49)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("Seek(49.999)", () => composer.SeekTo(49.999)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("Seek(99)", () => composer.SeekTo(99)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("Seek(99.999)", () => composer.SeekTo(99.999)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("Seek(174)", () => composer.SeekTo(174)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("Seek(349)", () => composer.SeekTo(349)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 350", () => clock.CurrentTime == 350); - AddStep("Seek(399)", () => composer.SeekTo(399)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("Seek(449)", () => composer.SeekTo(449)); - AddStep("SeekForward, Snap", () => composer.SeekForward(true)); - AddAssert("Time = 450", () => clock.CurrentTime == 450); + AddStep("Seek(49)", () => Clock.Seek(49)); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 50", () => Clock.CurrentTime == 50); + AddStep("Seek(49.999)", () => Clock.Seek(49.999)); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 50", () => Clock.CurrentTime == 50); + AddStep("Seek(99)", () => Clock.Seek(99)); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 100", () => Clock.CurrentTime == 100); + AddStep("Seek(99.999)", () => Clock.Seek(99.999)); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 100", () => Clock.CurrentTime == 100); + AddStep("Seek(174)", () => Clock.Seek(174)); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 175", () => Clock.CurrentTime == 175); + AddStep("Seek(349)", () => Clock.Seek(349)); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 350", () => Clock.CurrentTime == 350); + AddStep("Seek(399)", () => Clock.Seek(399)); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 400", () => Clock.CurrentTime == 400); + AddStep("Seek(449)", () => Clock.Seek(449)); + AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); + AddAssert("Time = 450", () => Clock.CurrentTime == 450); } /// @@ -249,19 +218,19 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(450)", () => composer.SeekTo(450)); - AddStep("SeekBackward", () => composer.SeekBackward()); - AddAssert("Time = 425", () => clock.CurrentTime == 425); - AddStep("SeekBackward", () => composer.SeekBackward()); - AddAssert("Time = 375", () => clock.CurrentTime == 375); - AddStep("SeekBackward", () => composer.SeekBackward()); - AddAssert("Time = 325", () => clock.CurrentTime == 325); - AddStep("SeekBackward", () => composer.SeekBackward()); - AddAssert("Time = 125", () => clock.CurrentTime == 125); - AddStep("SeekBackward", () => composer.SeekBackward()); - AddAssert("Time = 25", () => clock.CurrentTime == 25); - AddStep("SeekBackward", () => composer.SeekBackward()); - AddAssert("Time = 0", () => clock.CurrentTime == 0); + AddStep("Seek(450)", () => Clock.Seek(450)); + AddStep("SeekBackward", () => Clock.SeekBackward()); + AddAssert("Time = 425", () => Clock.CurrentTime == 425); + AddStep("SeekBackward", () => Clock.SeekBackward()); + AddAssert("Time = 375", () => Clock.CurrentTime == 375); + AddStep("SeekBackward", () => Clock.SeekBackward()); + AddAssert("Time = 325", () => Clock.CurrentTime == 325); + AddStep("SeekBackward", () => Clock.SeekBackward()); + AddAssert("Time = 125", () => Clock.CurrentTime == 125); + AddStep("SeekBackward", () => Clock.SeekBackward()); + AddAssert("Time = 25", () => Clock.CurrentTime == 25); + AddStep("SeekBackward", () => Clock.SeekBackward()); + AddAssert("Time = 0", () => Clock.CurrentTime == 0); } /// @@ -271,19 +240,19 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(450)", () => composer.SeekTo(450)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 350", () => clock.CurrentTime == 350); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 175", () => clock.CurrentTime == 175); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 100", () => clock.CurrentTime == 100); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 50", () => clock.CurrentTime == 50); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 0", () => clock.CurrentTime == 0); + AddStep("Seek(450)", () => Clock.Seek(450)); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 400", () => Clock.CurrentTime == 400); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 350", () => Clock.CurrentTime == 350); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 175", () => Clock.CurrentTime == 175); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 100", () => Clock.CurrentTime == 100); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 50", () => Clock.CurrentTime == 50); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 0", () => Clock.CurrentTime == 0); } /// @@ -294,18 +263,18 @@ namespace osu.Game.Tests.Visual { reset(); - AddStep("Seek(451)", () => composer.SeekTo(451)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 450", () => clock.CurrentTime == 450); - AddStep("Seek(450.999)", () => composer.SeekTo(450.999)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 450", () => clock.CurrentTime == 450); - AddStep("Seek(401)", () => composer.SeekTo(401)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 400", () => clock.CurrentTime == 400); - AddStep("Seek(401.999)", () => composer.SeekTo(401.999)); - AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); - AddAssert("Time = 400", () => clock.CurrentTime == 400); + AddStep("Seek(451)", () => Clock.Seek(451)); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 450", () => Clock.CurrentTime == 450); + AddStep("Seek(450.999)", () => Clock.Seek(450.999)); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 450", () => Clock.CurrentTime == 450); + AddStep("Seek(401)", () => Clock.Seek(401)); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 400", () => Clock.CurrentTime == 400); + AddStep("Seek(401.999)", () => Clock.Seek(401.999)); + AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); + AddAssert("Time = 400", () => Clock.CurrentTime == 400); } /// @@ -317,34 +286,34 @@ namespace osu.Game.Tests.Visual double lastTime = 0; - AddStep("Seek(0)", () => composer.SeekTo(0)); + AddStep("Seek(0)", () => Clock.Seek(0)); for (int i = 0; i < 20; i++) { AddStep("SeekForward, Snap", () => { - lastTime = clock.CurrentTime; - composer.SeekForward(true); + lastTime = Clock.CurrentTime; + Clock.SeekForward(true); }); - AddAssert("Time > lastTime", () => clock.CurrentTime > lastTime); + AddAssert("Time > lastTime", () => Clock.CurrentTime > lastTime); } for (int i = 0; i < 20; i++) { AddStep("SeekBackward, Snap", () => { - lastTime = clock.CurrentTime; - composer.SeekBackward(true); + lastTime = Clock.CurrentTime; + Clock.SeekBackward(true); }); - AddAssert("Time < lastTime", () => clock.CurrentTime < lastTime); + AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime); } - AddAssert("Time = 0", () => clock.CurrentTime == 0); + AddAssert("Time = 0", () => Clock.CurrentTime == 0); } private void reset() { - AddStep("Reset", () => composer.SeekTo(0)); + AddStep("Reset", () => Clock.Seek(0)); } private class TestHitObjectComposer : HitObjectComposer @@ -359,13 +328,13 @@ namespace osu.Game.Tests.Visual private class TimingPointVisualiser : CompositeDrawable { - private readonly Track track; + private readonly double length; private readonly Drawable tracker; - public TimingPointVisualiser(Beatmap beatmap, Track track) + public TimingPointVisualiser(Beatmap beatmap, double length) { - this.track = track; + this.length = length; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -417,7 +386,7 @@ namespace osu.Game.Tests.Visual for (int i = 0; i < timingPoints.Count; i++) { TimingControlPoint next = i == timingPoints.Count - 1 ? null : timingPoints[i + 1]; - timelineContainer.Add(new TimingPointTimeline(timingPoints[i], next?.Time ?? beatmap.HitObjects.Last().StartTime, track.Length)); + timelineContainer.Add(new TimingPointTimeline(timingPoints[i], next?.Time ?? length, length)); } } @@ -425,7 +394,7 @@ namespace osu.Game.Tests.Visual { base.Update(); - tracker.X = (float)(Time.Current / track.Length); + tracker.X = (float)(Time.Current / length); } private class TimingPointTimeline : CompositeDrawable diff --git a/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs index bbe2956c5d..25ea3443ba 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs @@ -6,36 +6,22 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using OpenTK; using osu.Game.Screens.Edit.Components.Timelines.Summary; -using osu.Framework.Configuration; -using osu.Framework.Timing; using osu.Game.Rulesets.Osu; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { [TestFixture] - public class TestCaseEditorSummaryTimeline : OsuTestCase + public class TestCaseEditorSummaryTimeline : EditorClockTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(SummaryTimeline) }; - private readonly Bindable beatmap = new Bindable(); - - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); - [BackgroundDependencyLoader] - private void load() + private void load(OsuGameBase osuGame) { - beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); - - var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - dependencies.CacheAs(clock); - dependencies.CacheAs(clock); + osuGame.Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); SummaryTimeline summaryTimeline; Add(summaryTimeline = new SummaryTimeline @@ -45,7 +31,7 @@ namespace osu.Game.Tests.Visual Size = new Vector2(500, 50) }); - summaryTimeline.Beatmap.BindTo(beatmap); + summaryTimeline.Beatmap.BindTo(osuGame.Beatmap); } } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 7050e34712..3afb22f0ad 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -35,11 +35,13 @@ namespace osu.Game.Database /// /// Fired when a new becomes available in the database. + /// This is not guaranteed to run on the update thread. /// public event Action ItemAdded; /// /// Fired when a is removed from the database. + /// This is not guaranteed to run on the update thread. /// public event Action ItemRemoved; diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 798ed1b9b3..946d13c02a 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -37,13 +37,7 @@ namespace osu.Game.Online.API public Bindable LocalUser { get; } = new Bindable(createGuestUser()); - public string Token - { - get { return authentication.Token?.ToString(); } - set { authentication.Token = string.IsNullOrEmpty(value) ? null : OAuthToken.Parse(value); } - } - - protected bool HasLogin => Token != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password); + protected bool HasLogin => authentication.Token.Value != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); @@ -57,11 +51,15 @@ namespace osu.Game.Online.API log = Logger.GetLogger(LoggingTarget.Network); ProvidedUsername = config.Get(OsuSetting.Username); - Token = config.Get(OsuSetting.Token); + + authentication.TokenString = config.Get(OsuSetting.Token); + authentication.Token.ValueChanged += onTokenChanged; Task.Factory.StartNew(run, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } + private void onTokenChanged(OAuthToken token) => config.Set(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty); + private readonly List components = new List(); internal new void Schedule(Action action) => base.Schedule(action); @@ -306,9 +304,6 @@ namespace osu.Game.Online.API { base.Dispose(isDisposing); - config.Set(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? Token : string.Empty); - config.Save(); - flushQueue(); cancellationToken.Cancel(); } diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index df8af7b8dc..af01fc99a9 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Diagnostics; +using osu.Framework.Configuration; using osu.Framework.IO.Network; namespace osu.Game.Online.API @@ -12,7 +13,13 @@ namespace osu.Game.Online.API private readonly string clientSecret; private readonly string endpoint; - public OAuthToken Token; + public readonly Bindable Token = new Bindable(); + + public string TokenString + { + get => Token.Value?.ToString(); + set => Token.Value = string.IsNullOrEmpty(value) ? null : OAuthToken.Parse(value); + } internal OAuth(string clientId, string clientSecret, string endpoint) { @@ -47,7 +54,7 @@ namespace osu.Game.Online.API return false; } - Token = req.ResponseObject; + Token.Value = req.ResponseObject; return true; } } @@ -66,14 +73,14 @@ namespace osu.Game.Online.API { req.Perform(); - Token = req.ResponseObject; + Token.Value = req.ResponseObject; return true; } } catch { //todo: potentially only kill the refresh token on certain exception types. - Token = null; + Token.Value = null; return false; } } @@ -95,15 +102,15 @@ namespace osu.Game.Online.API if (accessTokenValid) return true; // if not, let's try using our refresh token to request a new access token. - if (!string.IsNullOrEmpty(Token?.RefreshToken)) + if (!string.IsNullOrEmpty(Token.Value?.RefreshToken)) // ReSharper disable once PossibleNullReferenceException - AuthenticateWithRefresh(Token.RefreshToken); + AuthenticateWithRefresh(Token.Value.RefreshToken); return accessTokenValid; } } - private bool accessTokenValid => Token?.IsValid ?? false; + private bool accessTokenValid => Token.Value?.IsValid ?? false; internal bool HasValidAccessToken => RequestAccessToken() != null; @@ -111,12 +118,12 @@ namespace osu.Game.Online.API { if (!ensureAccessToken()) return null; - return Token.AccessToken; + return Token.Value.AccessToken; } internal void Clear() { - Token = null; + Token.Value = null; } private class AccessTokenRequestRefresh : AccessTokenRequest diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 89447b8ed6..2f6f8ff348 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -36,6 +36,10 @@ using osu.Game.Overlays.Volume; namespace osu.Game { + /// + /// The full osu! experience. Builds on top of to add menus and binding logic + /// for initial components that are generally retrieved via DI. + /// public class OsuGame : OsuGameBase, IKeyBindingHandler { public Toolbar Toolbar; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 54a279e977..533a04286b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -34,6 +34,11 @@ using osu.Game.Skinning; namespace osu.Game { + /// + /// The most basic that can be used to host osu! components and systems. + /// Unlike , this class will not load any kind of UI, allowing it to be used + /// for provide dependencies to test cases without interfering with them. + /// public class OsuGameBase : Framework.Game, ICanAcceptFiles { protected OsuConfigManager LocalConfig; diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index b9a35ec1f0..8056dbff3c 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -246,11 +246,11 @@ namespace osu.Game.Overlays.BeatmapSet if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd; } - private void handleBeatmapAdd(BeatmapSetInfo beatmap) + private void handleBeatmapAdd(BeatmapSetInfo beatmap) => Schedule(() => { if (beatmap.OnlineBeatmapSetID == BeatmapSet?.OnlineBeatmapSetID) downloadButtonsContainer.FadeOut(transition_duration); - } + }); private void download(bool noVideo) { diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 8d8a4aebaa..3f1aa04c36 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -188,12 +188,12 @@ namespace osu.Game.Overlays beatmaps.ItemAdded += setAdded; } - private void setAdded(BeatmapSetInfo set) + private void setAdded(BeatmapSetInfo set) => Schedule(() => { // if a new map was imported, we should remove it from search results (download completed etc.) panels?.FirstOrDefault(p => p.SetInfo.OnlineBeatmapSetID == set.OnlineBeatmapSetID)?.FadeOut(400).Expire(); BeatmapSets = BeatmapSets?.Where(b => b.OnlineBeatmapSetID != set.OnlineBeatmapSetID); - } + }); private void updateResultCounts() { @@ -323,6 +323,14 @@ namespace osu.Game.Overlays private int distinctCount(List list) => list.Distinct().ToArray().Length; + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmaps != null) + beatmaps.ItemAdded -= setAdded; + } + public class ResultCounts { public readonly int Artists; diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index ac7ec6257b..c981e5f493 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -74,8 +74,8 @@ namespace osu.Game.Overlays.Music }, }; - beatmaps.ItemAdded += list.AddBeatmapSet; - beatmaps.ItemRemoved += list.RemoveBeatmapSet; + beatmaps.ItemAdded += handleBeatmapAdded; + beatmaps.ItemRemoved += handleBeatmapRemoved; list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); @@ -95,6 +95,9 @@ namespace osu.Game.Overlays.Music beatmapBacking.TriggerChange(); } + private void handleBeatmapAdded(BeatmapSetInfo setInfo) => Schedule(() => list.AddBeatmapSet(setInfo)); + private void handleBeatmapRemoved(BeatmapSetInfo setInfo) => Schedule(() => list.RemoveBeatmapSet(setInfo)); + protected override void PopIn() { filter.Search.HoldFocus = true; @@ -153,6 +156,17 @@ namespace osu.Game.Overlays.Music track.Restart(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmaps != null) + { + beatmaps.ItemAdded -= handleBeatmapAdded; + beatmaps.ItemRemoved -= handleBeatmapRemoved; + } + } } //todo: placeholder diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5df5304751..a2215035dd 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -21,9 +21,13 @@ namespace osu.Game.Overlays.Settings.Sections public override FontAwesome Icon => FontAwesome.fa_paint_brush; + private SkinManager skins; + [BackgroundDependencyLoader] private void load(OsuConfigManager config, SkinManager skins) { + this.skins = skins; + FlowContent.Spacing = new Vector2(0, 5); Children = new Drawable[] { @@ -47,15 +51,29 @@ namespace osu.Game.Overlays.Settings.Sections }, }; - void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); - skins.ItemAdded += _ => reloadSkins(); - skins.ItemRemoved += _ => reloadSkins(); + skins.ItemAdded += onItemsChanged; + skins.ItemRemoved += onItemsChanged; reloadSkins(); skinDropdown.Bindable = config.GetBindable(OsuSetting.Skin); } + private void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); + + private void onItemsChanged(SkinInfo _) => Schedule(reloadSkins); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skins != null) + { + skins.ItemAdded -= onItemsChanged; + skins.ItemRemoved -= onItemsChanged; + } + } + private class SizeSlider : OsuSliderBar { public override string TooltipText => Current.Value.ToString(@"0.##x"); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 9b33ad2563..1e7416e63d 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -4,20 +4,16 @@ using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Framework.Logging; -using osu.Framework.MathUtils; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Edit.Screens.Compose; using osu.Game.Screens.Edit.Screens.Compose.Layers; using osu.Game.Screens.Edit.Screens.Compose.RadioButtons; @@ -33,9 +29,6 @@ namespace osu.Game.Rulesets.Edit private readonly List layerContainers = new List(); private readonly Bindable beatmap = new Bindable(); - private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - - private IAdjustableClock adjustableClock; protected HitObjectComposer(Ruleset ruleset) { @@ -44,14 +37,9 @@ namespace osu.Game.Rulesets.Edit RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader(true)] - private void load([NotNull] OsuGameBase osuGame, [NotNull] IAdjustableClock adjustableClock, [NotNull] IFrameBasedClock framedClock, [CanBeNull] BindableBeatDivisor beatDivisor) + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame, IFrameBasedClock framedClock) { - this.adjustableClock = adjustableClock; - - if (beatDivisor != null) - this.beatDivisor.BindTo(beatDivisor); - beatmap.BindTo(osuGame.Beatmap); try @@ -135,106 +123,6 @@ namespace osu.Game.Rulesets.Edit }); } - protected override bool OnWheel(InputState state) - { - if (state.Mouse.WheelDelta > 0) - SeekBackward(true); - else - SeekForward(true); - return true; - } - - /// - /// Seeks the current time one beat-snapped beat-length backwards. - /// - /// Whether to snap to the closest beat. - public void SeekBackward(bool snapped = false) => seek(-1, snapped); - - /// - /// Seeks the current time one beat-snapped beat-length forwards. - /// - /// Whether to snap to the closest beat. - public void SeekForward(bool snapped = false) => seek(1, snapped); - - private void seek(int direction, bool snapped) - { - var cpi = beatmap.Value.Beatmap.ControlPointInfo; - - var timingPoint = cpi.TimingPointAt(adjustableClock.CurrentTime); - if (direction < 0 && timingPoint.Time == adjustableClock.CurrentTime) - { - // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into - int activeIndex = cpi.TimingPoints.IndexOf(timingPoint); - while (activeIndex > 0 && adjustableClock.CurrentTime == timingPoint.Time) - timingPoint = cpi.TimingPoints[--activeIndex]; - } - - double seekAmount = timingPoint.BeatLength / beatDivisor; - double seekTime = adjustableClock.CurrentTime + seekAmount * direction; - - if (!snapped || cpi.TimingPoints.Count == 0) - { - adjustableClock.Seek(seekTime); - return; - } - - // We will be snapping to beats within timingPoint - seekTime -= timingPoint.Time; - - // Determine the index from timingPoint of the closest beat to seekTime, accounting for scrolling direction - int closestBeat; - if (direction > 0) - closestBeat = (int)Math.Floor(seekTime / seekAmount); - else - closestBeat = (int)Math.Ceiling(seekTime / seekAmount); - - seekTime = timingPoint.Time + closestBeat * seekAmount; - - // Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this. - // Instead, we'll go to the next beat in the direction when this is the case - if (Precision.AlmostEquals(adjustableClock.CurrentTime, seekTime)) - { - closestBeat += direction > 0 ? 1 : -1; - seekTime = timingPoint.Time + closestBeat * seekAmount; - } - - if (seekTime < timingPoint.Time && timingPoint != cpi.TimingPoints.First()) - seekTime = timingPoint.Time; - - var nextTimingPoint = cpi.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); - if (seekTime > nextTimingPoint?.Time) - seekTime = nextTimingPoint.Time; - - adjustableClock.Seek(seekTime); - } - - public void SeekTo(double seekTime, bool snapped = false) - { - if (!snapped) - { - adjustableClock.Seek(seekTime); - return; - } - - var timingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(seekTime); - double beatSnapLength = timingPoint.BeatLength / beatDivisor; - - // We will be snapping to beats within the timing point - seekTime -= timingPoint.Time; - - // Determine the index from the current timing point of the closest beat to seekTime - int closestBeat = (int)Math.Round(seekTime / beatSnapLength); - seekTime = timingPoint.Time + closestBeat * beatSnapLength; - - // Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to - // the next timing point's start time - var nextTimingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); - if (seekTime > nextTimingPoint?.Time) - seekTime = nextTimingPoint.Time; - - adjustableClock.Seek(seekTime); - } - private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool; protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true); diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index b249713581..9efe93c5a7 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -55,11 +55,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts if (Beatmap.Value.Track.Length == double.PositiveInfinity) return; float markerPos = MathHelper.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); - seekTo(markerPos / DrawWidth * Beatmap.Value.Track.Length); + adjustableClock.Seek(markerPos / DrawWidth * Beatmap.Value.Track.Length); } - private void seekTo(double time) => adjustableClock.Seek(time); - protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8b651000fd..e6edc9a6ff 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -12,6 +12,7 @@ using osu.Game.Screens.Edit.Menus; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Timing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Screens; @@ -32,6 +33,10 @@ namespace osu.Game.Screens.Edit private EditorScreen currentScreen; + private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); + + private EditorClock clock; + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) @@ -42,11 +47,12 @@ namespace osu.Game.Screens.Edit { // TODO: should probably be done at a RulesetContainer level to share logic with Player. var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); - var adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - adjustableClock.ChangeSource(sourceClock); + clock = new EditorClock(Beatmap.Value.Beatmap.ControlPointInfo, beatDivisor) { IsCoupled = false }; + clock.ChangeSource(sourceClock); - dependencies.CacheAs(adjustableClock); - dependencies.CacheAs(adjustableClock); + dependencies.CacheAs(clock); + dependencies.CacheAs(clock); + dependencies.Cache(beatDivisor); EditorMenuBar menuBar; TimeInfoContainer timeInfo; @@ -147,7 +153,6 @@ namespace osu.Game.Screens.Edit menuBar.Mode.ValueChanged += onModeChanged; bottomBackground.Colour = colours.Gray2; - } private void exportBeatmap() @@ -176,6 +181,15 @@ namespace osu.Game.Screens.Edit screenContainer.Add(currentScreen); } + protected override bool OnWheel(InputState state) + { + if (state.Mouse.WheelDelta > 0) + clock.SeekBackward(true); + else + clock.SeekForward(true); + return true; + } + protected override void OnResuming(Screen last) { Beatmap.Value.Track?.Stop(); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs new file mode 100644 index 0000000000..874fd186f8 --- /dev/null +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -0,0 +1,117 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.MathUtils; +using osu.Framework.Timing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Edit.Screens.Compose; + +namespace osu.Game.Screens.Edit +{ + /// + /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor. + /// + public class EditorClock : DecoupleableInterpolatingFramedClock + { + public ControlPointInfo ControlPointInfo; + + private readonly BindableBeatDivisor beatDivisor; + + public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor) + { + this.beatDivisor = beatDivisor; + + ControlPointInfo = controlPointInfo; + } + + /// + /// Seek to the closest snappable beat from a time. + /// + /// The raw position which should be seeked around. + /// Whether the seek could be performed. + public bool SeekSnapped(double position) + { + var timingPoint = ControlPointInfo.TimingPointAt(position); + double beatSnapLength = timingPoint.BeatLength / beatDivisor; + + // We will be snapping to beats within the timing point + position -= timingPoint.Time; + + // Determine the index from the current timing point of the closest beat to position + int closestBeat = (int)Math.Round(position / beatSnapLength); + position = timingPoint.Time + closestBeat * beatSnapLength; + + // Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to + // the next timing point's start time + var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); + if (position > nextTimingPoint?.Time) + position = nextTimingPoint.Time; + + return Seek(position); + } + + /// + /// Seeks backwards by one beat length. + /// + /// Whether to snap to the closest beat after seeking. + public void SeekBackward(bool snapped = false) => seek(-1, snapped); + + /// + /// Seeks forwards by one beat length. + /// + /// Whether to snap to the closest beat after seeking. + public void SeekForward(bool snapped = false) => seek(1, snapped); + + private void seek(int direction, bool snapped) + { + var timingPoint = ControlPointInfo.TimingPointAt(CurrentTime); + if (direction < 0 && timingPoint.Time == CurrentTime) + { + // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into + int activeIndex = ControlPointInfo.TimingPoints.IndexOf(timingPoint); + while (activeIndex > 0 && CurrentTime == timingPoint.Time) + timingPoint = ControlPointInfo.TimingPoints[--activeIndex]; + } + + double seekAmount = timingPoint.BeatLength / beatDivisor; + double seekTime = CurrentTime + seekAmount * direction; + + if (!snapped || ControlPointInfo.TimingPoints.Count == 0) + { + Seek(seekTime); + return; + } + + // We will be snapping to beats within timingPoint + seekTime -= timingPoint.Time; + + // Determine the index from timingPoint of the closest beat to seekTime, accounting for scrolling direction + int closestBeat; + if (direction > 0) + closestBeat = (int)Math.Floor(seekTime / seekAmount); + else + closestBeat = (int)Math.Ceiling(seekTime / seekAmount); + + seekTime = timingPoint.Time + closestBeat * seekAmount; + + // Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this. + // Instead, we'll go to the next beat in the direction when this is the case + if (Precision.AlmostEquals(CurrentTime, seekTime)) + { + closestBeat += direction > 0 ? 1 : -1; + seekTime = timingPoint.Time + closestBeat * seekAmount; + } + + if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First()) + seekTime = timingPoint.Time; + + var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); + if (seekTime > nextTimingPoint?.Time) + seekTime = nextTimingPoint.Time; + + Seek(seekTime); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs index 91adc8324a..bd672451c0 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using JetBrains.Annotations; using osu.Framework.Allocation; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; @@ -21,15 +22,11 @@ namespace osu.Game.Screens.Edit.Screens.Compose private Container composerContainer; - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); - - [BackgroundDependencyLoader] - private void load() + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] BindableBeatDivisor beatDivisor) { - dependencies.Cache(beatDivisor); + if (beatDivisor != null) + this.beatDivisor.BindTo(beatDivisor); ScrollableTimeline timeline; Children = new Drawable[] diff --git a/osu.Game/Tests/Visual/EditorClockTestCase.cs b/osu.Game/Tests/Visual/EditorClockTestCase.cs new file mode 100644 index 0000000000..bcdaa2f412 --- /dev/null +++ b/osu.Game/Tests/Visual/EditorClockTestCase.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Input; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Screens.Compose; + +namespace osu.Game.Tests.Visual +{ + /// + /// Provides a clock, beat-divisor, and scrolling capability for test cases of editor components that + /// are preferrably tested within the presence of a clock and seek controls. + /// + public abstract class EditorClockTestCase : OsuTestCase + { + protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); + protected readonly EditorClock Clock; + + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); + + private OsuGameBase osuGame; + + protected EditorClockTestCase() + { + Clock = new EditorClock(new ControlPointInfo(), BeatDivisor) { IsCoupled = false }; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + this.osuGame = osuGame; + + dependencies.Cache(BeatDivisor); + dependencies.CacheAs(Clock); + dependencies.CacheAs(Clock); + + osuGame.Beatmap.ValueChanged += beatmapChanged; + beatmapChanged(osuGame.Beatmap.Value); + } + + private void beatmapChanged(WorkingBeatmap working) + { + Clock.ControlPointInfo = working.Beatmap.ControlPointInfo; + Clock.ChangeSource((IAdjustableClock)working.Track ?? new StopwatchClock()); + Clock.ProcessFrame(); + } + + protected override void Update() + { + base.Update(); + + Clock.ProcessFrame(); + } + + protected override bool OnWheel(InputState state) + { + if (state.Mouse.WheelDelta > 0) + Clock.SeekBackward(true); + else + Clock.SeekForward(true); + + return true; + } + + protected override void Dispose(bool isDisposing) + { + osuGame.Beatmap.ValueChanged -= beatmapChanged; + + base.Dispose(isDisposing); + } + } +}