// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Background { [TestFixture] public partial class TestSceneBackgroundScreenDefault : OsuTestScene { private BackgroundScreenStack stack; private TestBackgroundScreenDefault screen; private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType().FirstOrDefault(); [Resolved] private SkinManager skins { get; set; } [Resolved] private OsuConfigManager config { get; set; } [Resolved] private IRenderer renderer { get; set; } [SetUpSteps] public void SetUpSteps() { AddStep("create background stack", () => Child = stack = new BackgroundScreenStack()); AddStep("push default screen", () => stack.Push(screen = new TestBackgroundScreenDefault())); AddUntilStep("wait for screen to load", () => screen.IsCurrentScreen()); } [Test] public void TestBeatmapBackgroundTracksBeatmap() { setSupporter(true); setSourceMode(BackgroundSource.Beatmap); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); AddAssert("background changed", () => screen.CheckLastLoadChange() == true); Graphics.Backgrounds.Background last = null; AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground)); AddStep("store background", () => last = getCurrentBackground()); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true); AddUntilStep("background is new beatmap background", () => last != getCurrentBackground()); AddStep("store background", () => last = getCurrentBackground()); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true); AddUntilStep("background is new beatmap background", () => last != getCurrentBackground()); } [Test] public void TestBeatmapBackgroundTracksBeatmapWhenSuspended() { setSupporter(true); setSourceMode(BackgroundSource.Beatmap); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); AddAssert("background changed", () => screen.CheckLastLoadChange() == true); AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground)); BackgroundScreenBeatmap nestedScreen = null; // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack. AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); AddUntilStep("previous background hidden", () => !screen.IsAlive); AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); AddStep("pop screen back to top level", () => screen.MakeCurrent()); AddAssert("top level background changed", () => screen.CheckLastLoadChange() == true); } [Test] public void TestBeatmapBackgroundIgnoresNoChangeWhenSuspended() { BackgroundScreenBeatmap nestedScreen = null; WorkingBeatmap originalWorking = null; setSupporter(true); setSourceMode(BackgroundSource.Beatmap); AddStep("change beatmap", () => originalWorking = Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); AddAssert("background changed", () => screen.CheckLastLoadChange() == true); AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground)); // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack. AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); // we're testing a case where scheduling may be used to avoid issues, so ensure the scheduler is no longer running. AddUntilStep("wait for top level not alive", () => !screen.IsAlive); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); AddStep("change beatmap back", () => Beatmap.Value = originalWorking); AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); AddStep("pop screen back to top level", () => screen.MakeCurrent()); AddStep("top level screen is current", () => screen.IsCurrentScreen()); AddAssert("top level background reused existing", () => screen.CheckLastLoadChange() == false); } [Test] public void TestBeatmapBackgroundWithStoryboardClockAlwaysUsesCurrentTrack() { BackgroundScreenBeatmap nestedScreen = null; WorkingBeatmap originalWorking = null; setSupporter(true); setSourceMode(BackgroundSource.BeatmapWithStoryboard); AddStep("change beatmap", () => originalWorking = Beatmap.Value = createTestWorkingBeatmapWithStoryboard()); AddAssert("background changed", () => screen.CheckLastLoadChange() == true); AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard)); AddStep("start music", () => MusicController.Play()); AddUntilStep("storyboard clock running", () => screen.ChildrenOfType().SingleOrDefault()?.Clock.IsRunning == true); // of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack. AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); // we're testing a case where scheduling may be used to avoid issues, so ensure the scheduler is no longer running. AddUntilStep("wait for top level not alive", () => !screen.IsAlive); AddStep("stop music", () => MusicController.Stop()); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithStoryboard()); AddStep("change beatmap back", () => Beatmap.Value = originalWorking); AddStep("restart music", () => MusicController.Play()); AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null); AddStep("pop screen back to top level", () => screen.MakeCurrent()); AddStep("top level screen is current", () => screen.IsCurrentScreen()); AddAssert("top level background reused existing", () => screen.CheckLastLoadChange() == false); AddUntilStep("storyboard clock running", () => screen.ChildrenOfType().Single().Clock.IsRunning); AddStep("stop music", () => MusicController.Stop()); AddStep("restore default beatmap", () => Beatmap.SetDefault()); } [Test] public void TestBeatmapBackgroundWithStoryboardUnloadedOnSuspension() { BackgroundScreenBeatmap nestedScreen = null; setSupporter(true); setSourceMode(BackgroundSource.BeatmapWithStoryboard); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithStoryboard()); AddAssert("background changed", () => screen.CheckLastLoadChange() == true); AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard)); AddUntilStep("storyboard present", () => screen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); AddUntilStep("storyboard unloaded", () => !screen.ChildrenOfType().Any()); AddStep("go back", () => screen.MakeCurrent()); AddUntilStep("storyboard reloaded", () => screen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); } [Test] public void TestBeatmapBackgroundWithStoryboardButBeatmapHasNone() { BackgroundScreenBeatmap nestedScreen = null; setSupporter(true); setSourceMode(BackgroundSource.BeatmapWithStoryboard); AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); AddAssert("background changed", () => screen.CheckLastLoadChange() == true); AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard)); AddUntilStep("no storyboard loaded", () => !screen.ChildrenOfType().Any()); AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); AddUntilStep("still no storyboard", () => !screen.ChildrenOfType().Any()); AddStep("go back", () => screen.MakeCurrent()); AddUntilStep("still no storyboard", () => !screen.ChildrenOfType().Any()); } [Test] public void TestBackgroundTypeSwitch() { setSupporter(true); setSourceMode(BackgroundSource.Beatmap); AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground); setSourceMode(BackgroundSource.BeatmapWithStoryboard); AddUntilStep("is storyboard background", () => getCurrentBackground() is BeatmapBackgroundWithStoryboard); setSourceMode(BackgroundSource.Skin); AddUntilStep("is default background", () => getCurrentBackground().GetType() == typeof(Graphics.Backgrounds.Background)); setCustomSkin(); AddUntilStep("is skin background", () => getCurrentBackground() is SkinBackground); } [Test] public void TestTogglingSupporterTogglesBeatmapBackground() { setSourceMode(BackgroundSource.Beatmap); setSupporter(true); AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground); setSupporter(false); AddUntilStep("is default background", () => !(getCurrentBackground() is BeatmapBackground)); setSupporter(true); AddUntilStep("is beatmap background", () => getCurrentBackground() is BeatmapBackground); } [TestCase(BackgroundSource.Beatmap, typeof(BeatmapBackground))] [TestCase(BackgroundSource.BeatmapWithStoryboard, typeof(BeatmapBackgroundWithStoryboard))] [TestCase(BackgroundSource.Skin, typeof(SkinBackground))] public void TestBackgroundDoesntReloadOnNoChange(BackgroundSource source, Type backgroundType) { setSourceMode(source); setSupporter(true); if (source == BackgroundSource.Skin) setCustomSkin(); AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == backgroundType); AddAssert("next doesn't load new background", () => screen.Next() == false); } [Test] public void TestBackgroundCyclingOnDefaultSkin([Values] bool supporter) { setSourceMode(BackgroundSource.Skin); setSupporter(supporter); setDefaultSkin(); AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background)); AddAssert("next cycles background", () => screen.Next()); } private void setSourceMode(BackgroundSource source) => AddStep($"set background mode to {source}", () => config.SetValue(OsuSetting.MenuBackgroundSource, source)); private void setSupporter(bool isSupporter) => AddStep($"set supporter {isSupporter}", () => ((DummyAPIAccess)API).LocalUser.Value = new APIUser { IsSupporter = isSupporter, Id = API.LocalUser.Value.Id + 1, }); private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(renderer, Audio); private WorkingBeatmap createTestWorkingBeatmapWithStoryboard() => new TestWorkingBeatmapWithStoryboard(Audio); private partial class TestBackgroundScreenDefault : BackgroundScreenDefault { private bool? lastLoadTriggerCausedChange; public override bool Next() { bool didChange = base.Next(); lastLoadTriggerCausedChange = didChange; return didChange; } public bool? CheckLastLoadChange() { bool? lastChange = lastLoadTriggerCausedChange; lastLoadTriggerCausedChange = null; return lastChange; } } private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap { private readonly IRenderer renderer; public UniqueBackgroundTestWorkingBeatmap(IRenderer renderer, AudioManager audioManager) : base(new Beatmap(), null, audioManager) { this.renderer = renderer; } public override Texture GetBackground() => renderer.CreateTexture(1, 1); } private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap { public TestWorkingBeatmapWithStoryboard(AudioManager audioManager) : base(new Beatmap(), createStoryboard(), audioManager) { } protected override Track GetBeatmapTrack() => new TrackVirtual(100000); private static Storyboard createStoryboard() { var storyboard = new Storyboard(); storyboard.Layers.Last().Add(new TestStoryboardElement()); return storyboard; } private class TestStoryboardElement : IStoryboardElementWithDuration { public string Path => string.Empty; public bool IsDrawable => true; public double StartTime => double.MinValue; public double EndTime => double.MaxValue; public double EndTimeForDisplay => double.MaxValue; public Drawable CreateDrawable() => new DrawableTestStoryboardElement(); } private partial class DrawableTestStoryboardElement : OsuSpriteText { public override bool RemoveWhenNotAlive => false; public DrawableTestStoryboardElement() { Anchor = Origin = Anchor.Centre; Font = OsuFont.Default.With(size: 32); Text = "(not started)"; } protected override void Update() { base.Update(); Text = Time.Current.ToString("N2"); } } } private void setCustomSkin() { // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLiveUnmanaged()); } private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault()); [TearDownSteps] public void TearDown() => setDefaultSkin(); } }