diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneReplayRecording.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneReplayRecording.cs new file mode 100644 index 0000000000..57ee49d70d --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneReplayRecording.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Replays; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public partial class TestSceneReplayRecording : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + + [Resolved] + private AudioManager audioManager { get; set; } = null!; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + { + HitObjects = + { + new Fruit { StartTime = 0, }, + new Fruit { StartTime = 5000, }, + new Fruit { StartTime = 10000, }, + new Fruit { StartTime = 15000, } + } + }; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + + [Test] + public void TestRecording() + { + seekTo(0); + AddStep("start moving left", () => InputManager.PressKey(Key.Left)); + seekTo(5000); + AddStep("end moving left", () => InputManager.ReleaseKey(Key.Left)); + AddAssert("catcher max left", () => this.ChildrenOfType().Single().X, () => Is.EqualTo(0)); + AddAssert("movement to left recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([CatchAction.MoveLeft]))); + AddAssert("replay reached left edge", () => Player.Score.Replay.Frames.OfType().Any(f => Precision.AlmostEquals(f.Position, 0))); + + AddStep("start dashing right", () => + { + InputManager.PressKey(Key.LShift); + InputManager.PressKey(Key.Right); + }); + seekTo(10000); + AddStep("end dashing right", () => + { + InputManager.ReleaseKey(Key.LShift); + InputManager.ReleaseKey(Key.Right); + }); + AddAssert("catcher max right", () => this.ChildrenOfType().Single().X, () => Is.EqualTo(CatchPlayfield.WIDTH)); + AddAssert("dash to right recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([CatchAction.Dash, CatchAction.MoveRight]))); + AddAssert("replay reached right edge", () => Player.Score.Replay.Frames.OfType().Any(f => Precision.AlmostEquals(f.Position, CatchPlayfield.WIDTH))); + } + + private void seekTo(double time) + { + AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500)); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs new file mode 100644 index 0000000000..43c648a6dd --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneReplayRecording : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Resolved] + private AudioManager audioManager { get; set; } = null!; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = + { + new Note { StartTime = 0, }, + new Note { StartTime = 5000, }, + new Note { StartTime = 10000, }, + new Note { StartTime = 15000, } + }, + Difficulty = { CircleSize = 1 }, + BeatmapInfo = + { + Ruleset = ruleset, + } + }; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + + [Test] + public void TestRecording() + { + seekTo(0); + AddStep("press space", () => InputManager.Key(Key.Space)); + AddAssert("button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([ManiaAction.Key1]))); + } + + private void seekTo(double time) + { + AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs new file mode 100644 index 0000000000..d163e8a1b4 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneReplayRecording : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + [Resolved] + private AudioManager audioManager { get; set; } = null!; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + { + HitObjects = + { + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 0, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 5000, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 10000, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 15000, + } + } + }; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + + [Test] + public void TestRecording() + { + seekTo(0); + AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single())); + AddStep("press X", () => InputManager.Key(Key.X)); + AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton]))); + + seekTo(5000); + AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single())); + AddStep("press Z", () => InputManager.Key(Key.Z)); + AddAssert("left button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([OsuAction.LeftButton]))); + + seekTo(10000); + AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single())); + AddStep("press C", () => InputManager.Key(Key.C)); + AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke]))); + } + + private void seekTo(double time) + { + AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs new file mode 100644 index 0000000000..14a1fbfa99 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public partial class TestSceneReplayRecording : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset(); + + [Resolved] + private AudioManager audioManager { get; set; } = null!; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + { + HitObjects = + { + new Hit { StartTime = 0, }, + new Hit { StartTime = 5000, }, + new Hit { StartTime = 10000, }, + new Hit { StartTime = 15000, } + } + }; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + + [Test] + public void TestRecording() + { + seekTo(0); + AddStep("press D", () => InputManager.Key(Key.D)); + AddAssert("left rim press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftRim]))); + + seekTo(5000); + AddStep("press F", () => InputManager.Key(Key.F)); + AddAssert("left centre press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftCentre]))); + + seekTo(10000); + AddStep("press J", () => InputManager.Key(Key.J)); + AddAssert("right centre press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([TaikoAction.RightCentre]))); + + seekTo(10000); + AddStep("press K", () => InputManager.Key(Key.K)); + AddAssert("right rim press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([TaikoAction.RightRim]))); + } + + private void seekTo(double time) + { + AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time)); + AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500)); + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs deleted file mode 100644 index 5c6138596a..0000000000 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSongSelectNavigation.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Screens; -using osu.Game.Database; -using osu.Game.Overlays; -using osu.Game.Overlays.Toolbar; -using osu.Game.Screens; -using osu.Game.Screens.Footer; -using osu.Game.Screens.Menu; -using osu.Game.Screens.SelectV2; - -namespace osu.Game.Tests.Visual.Navigation -{ - [Explicit] - public partial class TestSceneSongSelectNavigation : ScreenTestScene - { - [Cached] - private readonly ScreenFooter screenFooter; - - [Cached] - private readonly OsuLogo logo; - - [Cached(typeof(INotificationOverlay))] - private readonly INotificationOverlay notificationOverlay = new NotificationOverlay(); - - protected override bool UseOnlineAPI => true; - - public TestSceneSongSelectNavigation() - { - Children = new Drawable[] - { - new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Toolbar - { - State = { Value = Visibility.Visible }, - }, - screenFooter = new ScreenFooter - { - OnBack = () => Stack.CurrentScreen.Exit(), - }, - logo = new OsuLogo - { - Alpha = 0f, - }, - }, - }, - }; - - Stack.Padding = new MarginPadding { Top = Toolbar.HEIGHT }; - } - - [BackgroundDependencyLoader] - private void load() - { - RealmDetachedBeatmapStore beatmapStore; - Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); - Add(beatmapStore); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Stack.ScreenPushed += updateFooter; - Stack.ScreenExited += updateFooter; - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - AddStep("load screen", () => Stack.Push(new SoloSongSelect())); - AddUntilStep("wait for load", () => Stack.CurrentScreen is SoloSongSelect songSelect && songSelect.IsLoaded); - } - - private void updateFooter(IScreen? _, IScreen? newScreen) - { - if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) - { - screenFooter.Show(); - screenFooter.SetButtons(osuScreen.CreateFooterButtons()); - } - else - { - screenFooter.Hide(); - screenFooter.SetButtons(Array.Empty()); - } - } - } -} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index f5574d2789..383ec47a69 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -5,7 +5,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Primitives; using osu.Framework.Testing; -using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -19,19 +18,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RemoveAllBeatmaps(); CreateCarousel(); - SortBy(SortMode.Artist); - AddBeatmaps(10); WaitForDrawablePanels(); } [Test] - public void TestScrollPositionMaintainedOnAddSecondSelected() + public void TestScrollPositionMaintainedOnRemove_SecondSelected() { Quad positionBefore = default; AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); @@ -45,11 +41,35 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } [Test] - public void TestScrollPositionMaintainedOnAddLastSelected() + public void TestScrollPositionMaintainedOnRemove_SecondSelected_WithUserScroll() { Quad positionBefore = default; - AddStep("scroll to last item", () => Scroll.ScrollToEnd(false)); + AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); + WaitForScrolling(); + + AddStep("override scroll with user scroll", () => + { + InputManager.MoveMouseTo(Scroll.ScreenSpaceDrawQuad.Centre); + InputManager.ScrollVerticalBy(-1); + }); + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + RemoveFirstBeatmap(); + WaitForFiltering(); + + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollPositionMaintainedOnRemove_LastSelected() + { + Quad positionBefore = default; + + AddStep("scroll to end", () => Scroll.ScrollToEnd(false)); AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last()); @@ -62,5 +82,50 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } + + [Test] + public void TestScrollToSelectionAfterFilter() + { + Quad positionBefore = default; + + AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First()); + + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + AddStep("scroll to end", () => Scroll.ScrollToEnd()); + WaitForScrolling(); + + ApplyToFilter("search", f => f.SearchText = "Some"); + WaitForFiltering(); + + AddUntilStep("select screen position returned to selection", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } + + [Test] + public void TestScrollToSelectionAfterFilter_WithUserScroll() + { + Quad positionBefore = default; + + AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First()); + WaitForScrolling(); + + AddStep("override scroll with user scroll", () => + { + InputManager.MoveMouseTo(Scroll.ScreenSpaceDrawQuad.Centre); + InputManager.ScrollVerticalBy(-1); + }); + WaitForScrolling(); + + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + + ApplyToFilter("search", f => f.SearchText = "Some"); + WaitForFiltering(); + + AddUntilStep("select screen position returned to selection", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + () => Is.EqualTo(positionBefore)); + } } } diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index ca6f50c9a3..e82190b445 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -315,6 +315,8 @@ namespace osu.Game.Graphics.Carousel HandleItemSelected(currentSelection.Model); refreshAfterSelection(); + if (!Scroll.UserScrolling) + scrollToSelection(); NewItemsPresented?.Invoke(); }); @@ -471,6 +473,9 @@ namespace osu.Game.Graphics.Carousel #region Selection handling + /// + /// Becomes invalid when the current selection has changed and needs to be updated visually. + /// private readonly Cached selectionValid = new Cached(); private Selection currentKeyboardSelection = new Selection(); @@ -571,7 +576,10 @@ namespace osu.Game.Graphics.Carousel if (!selectionValid.IsValid) { refreshAfterSelection(); + + // Always scroll to selection in this case (regardless of `UserScrolling` state), centering the selection. scrollToSelection(); + selectionValid.Validate(); } diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 1f91e2c5f0..d723c31434 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -40,8 +40,6 @@ namespace osu.Game.Rulesets.UI this.target = target; RelativeSizeAxes = Axes.Both; - - Depth = float.MinValue; } protected override void LoadComplete() diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c7d57f2993..fba321d128 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -43,6 +43,7 @@ using osu.Game.Seasonal; using osuTK; using osuTK.Graphics; using osu.Game.Localisation; +using osu.Game.Screens.SelectV2; namespace osu.Game.Screens.Menu { @@ -239,7 +240,13 @@ namespace osu.Game.Screens.Menu public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial; - private void loadSoloSongSelect() => this.Push(new PlaySongSelect()); + private void loadSoloSongSelect() + { + if (GetContainingInputManager()!.CurrentState.Keyboard.ControlPressed) + this.Push(new SoloSongSelect()); + else + this.Push(new PlaySongSelect()); + } public override void OnEntering(ScreenTransitionEvent e) {