// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; using osu.Game.Tests.Resources; using osuTK; namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneStoryboardCommands : OsuTestScene { [Cached(typeof(Storyboard))] private TestStoryboard storyboard { get; set; } = new TestStoryboard { UseSkinSprites = false, AlwaysProvideTexture = true, }; private readonly ManualClock manualClock = new ManualClock { Rate = 1, IsRunning = true }; private int clockDirection; private const string lookup_name = "hitcircleoverlay"; private const double clock_limit = 2500; protected override Container Content => content; private Container content = null!; private SpriteText timelineText = null!; private Box timelineMarker = null!; [BackgroundDependencyLoader] private void load() { base.Content.Children = new Drawable[] { content = new Container { RelativeSizeAxes = Axes.Both, }, timelineText = new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Margin = new MarginPadding { Bottom = 60 }, }, timelineMarker = new Box { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomCentre, RelativePositionAxes = Axes.X, Size = new Vector2(2, 50), }, }; } [SetUpSteps] public void SetUpSteps() { AddStep("start clock", () => clockDirection = 1); AddStep("pause clock", () => clockDirection = 0); AddStep("set clock = 0", () => manualClock.CurrentTime = 0); } [Test] public void TestNormalCommandPlayback() { AddStep("create storyboard", () => Child = createStoryboard(s => { s.Commands.AddY(Easing.OutBounce, 500, 900, 100, 240); s.Commands.AddY(Easing.OutQuint, 1100, 1500, 240, 100); })); assert(0, 100); assert(500, 100); assert(1000, 240); assert(1500, 100); assert(clock_limit, 100); assert(1500, 100); assert(1000, 240); assert(500, 100); assert(0, 100); void assert(double time, double y) { AddStep($"set clock = {time}", () => manualClock.CurrentTime = time); AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType().Single().Y == y); } } [Test] public void TestLoopingCommandsPlayback() { AddStep("create storyboard", () => Child = createStoryboard(s => { var loop = s.AddLoopingGroup(250, 1); loop.AddY(Easing.OutBounce, 0, 400, 100, 240); loop.AddY(Easing.OutQuint, 600, 1000, 240, 100); })); assert(0, 100); assert(250, 100); assert(850, 240); assert(1250, 100); assert(1850, 240); assert(2250, 100); assert(clock_limit, 100); assert(2250, 100); assert(1850, 240); assert(1250, 100); assert(850, 240); assert(250, 100); assert(0, 100); void assert(double time, double y) { AddStep($"set clock = {time}", () => manualClock.CurrentTime = time); AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType().Single().Y == y); } } [Test] public void TestLoopManyTimes() { AddStep("create storyboard", () => Child = createStoryboard(s => { var loop = s.AddLoopingGroup(500, 10000); loop.AddY(Easing.OutBounce, 0, 60, 100, 240); loop.AddY(Easing.OutQuint, 80, 120, 240, 100); })); } [Test] public void TestParameterTemporaryEffect() { AddStep("create storyboard", () => Child = createStoryboard(s => { s.Commands.AddFlipV(Easing.None, 1000, 1500, true, false); })); AddAssert("sprite not flipped at t = 0", () => !this.ChildrenOfType().Single().FlipV); AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250); AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType().Single().FlipV); AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000); AddAssert("sprite not flipped at t = 2000", () => !this.ChildrenOfType().Single().FlipV); AddStep("resume clock", () => clockDirection = 1); } [Test] public void TestParameterPermanentEffect() { AddStep("create storyboard", () => Child = createStoryboard(s => { s.Commands.AddFlipV(Easing.None, 1000, 1000, true, true); })); AddAssert("sprite flipped at t = 0", () => this.ChildrenOfType().Single().FlipV); AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250); AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType().Single().FlipV); AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000); AddAssert("sprite flipped at t = 2000", () => this.ChildrenOfType().Single().FlipV); AddStep("resume clock", () => clockDirection = 1); } protected override void Update() { base.Update(); if (manualClock.CurrentTime > clock_limit || manualClock.CurrentTime < 0) clockDirection = -clockDirection; manualClock.CurrentTime += Time.Elapsed * clockDirection; timelineText.Text = $"Time: {manualClock.CurrentTime:0}ms"; timelineMarker.X = (float)(manualClock.CurrentTime / clock_limit); } private DrawableStoryboard createStoryboard(Action? addCommands = null) { var layer = storyboard.GetLayer("Background"); var sprite = new StoryboardSprite(lookup_name, Anchor.Centre, new Vector2(320, 240)); sprite.Commands.AddScale(Easing.None, 0, clock_limit, 0.5f, 0.5f); sprite.Commands.AddAlpha(Easing.None, 0, clock_limit, 1, 1); addCommands?.Invoke(sprite); layer.Elements.Clear(); layer.Add(sprite); return storyboard.CreateDrawable().With(c => c.Clock = new FramedClock(manualClock)); } private partial class TestStoryboard : Storyboard { public override DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) { return new TestDrawableStoryboard(this, mods); } public bool AlwaysProvideTexture { get; set; } public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty; private partial class TestDrawableStoryboard : DrawableStoryboard { private readonly bool alwaysProvideTexture; public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList? mods) : base(storyboard, mods) { alwaysProvideTexture = storyboard.AlwaysProvideTexture; } protected override IResourceStore CreateResourceLookupStore() => alwaysProvideTexture ? new AlwaysReturnsTextureStore() : new ResourceStore(); internal class AlwaysReturnsTextureStore : IResourceStore { private const string test_image = "Resources/Textures/test-image.png"; private readonly DllResourceStore store; public AlwaysReturnsTextureStore() { store = TestResources.GetStore(); } public void Dispose() => store.Dispose(); public byte[] Get(string name) => store.Get(test_image); public Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken); public Stream GetStream(string name) => store.GetStream(test_image); public IEnumerable GetAvailableResources() => store.GetAvailableResources(); } } } } }