diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 02c65db6ad..7ca311118e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Screens.Play; using osu.Game.Skinning; +using osu.Game.Storyboards; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests @@ -75,14 +76,14 @@ namespace osu.Game.Rulesets.Osu.Tests protected override Player CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin); - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) => new CustomSkinWorkingBeatmap(beatmap, Clock, audio, testBeatmapSkin); + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, audio, testBeatmapSkin); public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap { private readonly ISkinSource skin; - public CustomSkinWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin) - : base(beatmap, frameBasedClock, audio) + public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin) + : base(beatmap, storyboard, frameBasedClock, audio) { this.skin = skin; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index cded7f0e95..d0ce0c33c2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -15,6 +15,7 @@ using osu.Game.Tests.Visual; using osuTK; using System.Collections.Generic; using System.Linq; +using osu.Game.Storyboards; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Rulesets.Osu.Tests @@ -28,9 +29,9 @@ namespace osu.Game.Rulesets.Osu.Tests protected override bool Autoplay => true; - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) { - var working = new ClockBackedTestWorkingBeatmap(beatmap, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); track = (TrackVirtualManual)working.Track; return working; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index f94071a7a9..5ee109e3dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -7,6 +7,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; +using osu.Game.Storyboards; namespace osu.Game.Tests.Visual.Gameplay { @@ -29,9 +30,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("key counter reset", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) { - var working = base.CreateWorkingBeatmap(beatmap); + var working = base.CreateWorkingBeatmap(beatmap, storyboard); track = (ClockBackedTestWorkingBeatmap.TrackVirtualManual)working.Track; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index ffc025a942..b2b58a63fb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; +using osu.Game.Storyboards; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -35,9 +36,9 @@ namespace osu.Game.Tests.Visual.Gameplay private Track track; - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) { - var working = new ClockBackedTestWorkingBeatmap(beatmap, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); track = working.Track; return working; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs new file mode 100644 index 0000000000..0150c6ea74 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -0,0 +1,113 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneLeadIn : RateAdjustedBeatmapTestScene + { + private LeadInPlayer player; + + private const double lenience_ms = 10; + + private const double first_hit_object = 2170; + + [TestCase(1000, 0)] + [TestCase(2000, 0)] + [TestCase(3000, first_hit_object - 3000)] + [TestCase(10000, first_hit_object - 10000)] + public void TestLeadInProducesCorrectStartTime(double leadIn, double expectedStartTime) + { + loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) + { + BeatmapInfo = { AudioLeadIn = leadIn } + }); + + AddAssert($"first frame is {expectedStartTime}", () => + { + Debug.Assert(player.FirstFrameClockTime != null); + return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); + }); + } + + [TestCase(1000, 0)] + [TestCase(0, 0)] + [TestCase(-1000, -1000)] + [TestCase(-10000, -10000)] + public void TestStoryboardProducesCorrectStartTime(double firstStoryboardEvent, double expectedStartTime) + { + var storyboard = new Storyboard(); + + var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); + + storyboard.GetLayer("Background").Add(sprite); + + loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); + + AddAssert($"first frame is {expectedStartTime}", () => + { + Debug.Assert(player.FirstFrameClockTime != null); + return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); + }); + } + + private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + { + AddStep("create player", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap, storyboard); + LoadScreen(player = new LeadInPlayer()); + }); + + AddUntilStep("player loaded", () => player.IsLoaded && player.Alpha == 1); + } + + private class LeadInPlayer : TestPlayer + { + public LeadInPlayer() + : base(false, false) + { + } + + public double? FirstFrameClockTime; + + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + + public double GameplayStartTime => DrawableRuleset.GameplayStartTime; + + public double FirstHitObjectTime => DrawableRuleset.Objects.First().StartTime; + + public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (!FirstFrameClockTime.HasValue) + { + FirstFrameClockTime = GameplayClockContainer.GameplayClock.CurrentTime; + AddInternal(new OsuSpriteText + { + Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} " + + $"FirstHitObjectTime: {FirstHitObjectTime} " + + $"LeadInTime: {Beatmap.Value.BeatmapInfo.AudioLeadIn} " + + $"FirstFrameClockTime: {FirstFrameClockTime}" + }); + } + } + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs index 65b56319e8..4d701f56a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs @@ -6,6 +6,7 @@ using osu.Framework.Lists; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Screens.Play; +using osu.Game.Storyboards; namespace osu.Game.Tests.Visual.Gameplay { @@ -42,9 +43,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) { - var working = base.CreateWorkingBeatmap(beatmap); + var working = base.CreateWorkingBeatmap(beatmap, storyboard); workingWeakReferences.Add(working); return working; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index b152c21454..875e7b9758 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -4,8 +4,8 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Timing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osuTK; using osuTK.Input; @@ -18,25 +18,37 @@ namespace osu.Game.Tests.Visual.Gameplay private SkipOverlay skip; private int requestCount; + private double increment; + + private GameplayClockContainer gameplayClockContainer; + private GameplayClock gameplayClock; + + private const double skip_time = 6000; + [SetUp] public void SetUp() => Schedule(() => { requestCount = 0; - Child = new Container + increment = skip_time; + + Child = gameplayClockContainer = new GameplayClockContainer(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new Mod[] { }, 0) { RelativeSizeAxes = Axes.Both, - Clock = new FramedOffsetClock(Clock) - { - Offset = -Clock.CurrentTime, - }, Children = new Drawable[] { - skip = new SkipOverlay(6000) + skip = new SkipOverlay(skip_time) { - RequestSeek = _ => requestCount++ + RequestSkip = () => + { + requestCount++; + gameplayClockContainer.Seek(gameplayClock.CurrentTime + increment); + } } }, }; + + gameplayClockContainer.Start(); + gameplayClock = gameplayClockContainer.GameplayClock; }); [Test] @@ -64,19 +76,35 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestClickOnlyActuatesOnce() { AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); - AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("click", () => + { + increment = skip_time - gameplayClock.CurrentTime - GameplayClockContainer.MINIMUM_SKIP_TIME / 2; + InputManager.Click(MouseButton.Left); + }); AddStep("click", () => InputManager.Click(MouseButton.Left)); AddStep("click", () => InputManager.Click(MouseButton.Left)); AddStep("click", () => InputManager.Click(MouseButton.Left)); checkRequestCount(1); } + [Test] + public void TestClickOnlyActuatesMultipleTimes() + { + AddStep("set increment lower", () => increment = 3000); + AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + checkRequestCount(2); + } + [Test] public void TestDoesntFadeOnMouseDown() { AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("button down", () => InputManager.PressButton(MouseButton.Left)); - AddUntilStep("wait for overlay disapper", () => !skip.IsAlive); + AddUntilStep("wait for overlay disappear", () => !skip.IsPresent); AddAssert("ensure button didn't disappear", () => skip.Children.First().Alpha > 0); AddStep("button up", () => InputManager.ReleaseButton(MouseButton.Left)); checkRequestCount(0); diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 198046df4f..6e82c465dc 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -76,7 +76,7 @@ namespace osu.Game.Beatmaps public string MD5Hash { get; set; } // General - public int AudioLeadIn { get; set; } + public double AudioLeadIn { get; set; } public bool Countdown { get; set; } = true; public float StackLeniency { get; set; } = 0.7f; public bool SpecialStyle { get; set; } diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 379c4c89ed..d5f75f6ad1 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -34,7 +34,6 @@ namespace osu.Game.Screens.Play public void ProcessFrame() { // we do not want to process the underlying clock. - // this is handled by PauseContainer. } public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 2f2028ff53..0b8bab3ffc 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -41,6 +41,8 @@ namespace osu.Game.Screens.Play private readonly double gameplayStartTime; + private readonly double firstHitObjectTime; + public readonly Bindable UserPlaybackRate = new BindableDouble(1) { Default = 1, @@ -66,6 +68,7 @@ namespace osu.Game.Screens.Play this.beatmap = beatmap; this.mods = mods; this.gameplayStartTime = gameplayStartTime; + this.firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; RelativeSizeAxes = Axes.Both; @@ -89,6 +92,11 @@ namespace osu.Game.Screens.Play private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; + /// + /// Duration before gameplay start time required before skip button displays. + /// + public const double MINIMUM_SKIP_TIME = 1000; + private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); [BackgroundDependencyLoader] @@ -97,9 +105,22 @@ namespace osu.Game.Screens.Play userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); - UserPlaybackRate.ValueChanged += _ => updateRate(); + // sane default provided by ruleset. + double startTime = Math.Min(0, gameplayStartTime); - Seek(Math.Min(-beatmap.BeatmapInfo.AudioLeadIn, gameplayStartTime)); + // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. + // this is commonly used to display an intro before the audio track start. + startTime = Math.Min(startTime, beatmap.Storyboard.FirstEventTime); + + // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. + // this is not available as an option in the live editor but can still be applied via .osu editing. + if (beatmap.BeatmapInfo.AudioLeadIn > 0) + startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); + + Seek(startTime); + + adjustableClock.ProcessFrame(); + UserPlaybackRate.ValueChanged += _ => updateRate(); } public void Restart() @@ -130,6 +151,23 @@ namespace osu.Game.Screens.Play this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } + /// + /// Skip forward to the next valid skip point. + /// + public void Skip() + { + if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME) + return; + + double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME; + + if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) + // double skip exception for storyboards with very long intros + skipTarget = 0; + + Seek(skipTarget); + } + /// /// Seek to a specific time in gameplay. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a9b0649fab..d6488dc209 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -203,7 +203,7 @@ namespace osu.Game.Screens.Play }, new SkipOverlay(DrawableRuleset.GameplayStartTime) { - RequestSeek = GameplayClockContainer.Seek + RequestSkip = GameplayClockContainer.Skip }, FailOverlay = new FailOverlay { diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 835867fe62..1a5ed20953 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -19,6 +19,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.MathUtils; using osu.Game.Input.Bindings; namespace osu.Game.Screens.Play @@ -27,7 +28,7 @@ namespace osu.Game.Screens.Play { private readonly double startTime; - public Action RequestSeek; + public Action RequestSkip; private Button button; private Box remainingTimeBox; @@ -35,6 +36,9 @@ namespace osu.Game.Screens.Play private FadeContainer fadeContainer; private double displayTime; + [Resolved] + private GameplayClock gameplayClock { get; set; } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// @@ -45,8 +49,6 @@ namespace osu.Game.Screens.Play { this.startTime = startTime; - Show(); - RelativePositionAxes = Axes.Both; RelativeSizeAxes = Axes.X; @@ -57,13 +59,8 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, GameplayClock clock) + private void load(OsuColour colours) { - var baseClock = Clock; - - if (clock != null) - Clock = clock; - Children = new Drawable[] { fadeContainer = new FadeContainer @@ -73,7 +70,6 @@ namespace osu.Game.Screens.Play { button = new Button { - Clock = baseClock, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -90,46 +86,42 @@ namespace osu.Game.Screens.Play }; } - /// - /// Duration before gameplay start time required before skip button displays. - /// - private const double skip_buffer = 1000; - private const double fade_time = 300; - private double beginFadeTime => startTime - fade_time; + private double fadeOutBeginTime => startTime - GameplayClockContainer.MINIMUM_SKIP_TIME; protected override void LoadComplete() { base.LoadComplete(); // skip is not required if there is no extra "empty" time to skip. - if (Clock.CurrentTime > beginFadeTime - skip_buffer) + // we may need to remove this if rewinding before the initial player load position becomes a thing. + if (fadeOutBeginTime < gameplayClock.CurrentTime) { - Alpha = 0; Expire(); return; } - this.FadeInFromZero(fade_time); - using (BeginAbsoluteSequence(beginFadeTime)) - this.FadeOut(fade_time); + button.Action = () => RequestSkip?.Invoke(); + displayTime = gameplayClock.CurrentTime; - button.Action = () => RequestSeek?.Invoke(beginFadeTime); - - displayTime = Time.Current; - - Expire(); + Show(); } - protected override void PopIn() => this.FadeIn(); + protected override void PopIn() => this.FadeIn(fade_time); - protected override void PopOut() => this.FadeOut(); + protected override void PopOut() => this.FadeOut(fade_time); protected override void Update() { base.Update(); - remainingTimeBox.ResizeWidthTo((float)Math.Max(0, 1 - (Time.Current - displayTime) / (beginFadeTime - displayTime)), 120, Easing.OutQuint); + + var progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); + + remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); + + button.Enabled.Value = progress > 0; + State.Value = progress > 0 ? Visibility.Visible : Visibility.Hidden; } protected override bool OnMouseMove(MouseMoveEvent e) @@ -335,13 +327,7 @@ namespace osu.Game.Screens.Play box.FlashColour(Color4.White, 500, Easing.OutQuint); aspect.ScaleTo(1.2f, 2000, Easing.OutQuint); - bool result = base.OnClick(e); - - // for now, let's disable the skip button after the first press. - // this will likely need to be contextual in the future (bound from external components). - Enabled.Value = false; - - return result; + return base.OnClick(e); } } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 538656a5fa..17736e7819 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -640,10 +640,19 @@ namespace osu.Game.Screens.Select itemsCache.Validate(); } + private bool firstScroll = true; + private void updateScrollPosition() { if (scrollTarget != null) { + if (firstScroll) + { + // reduce movement when first displaying the carousel. + scroll.ScrollTo(scrollTarget.Value - 200, false); + firstScroll = false; + } + scroll.ScrollTo(scrollTarget.Value); scrollPositionCache.Validate(); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 5f1f5ddacb..3a117d1713 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -66,7 +66,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader] private void load(IBindable beatmap, TextureStore textureStore) { - var path = beatmap.Value.BeatmapSetInfo.Files.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; + var path = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; if (path == null) return; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 3d988c5fe3..401a7ce25a 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -17,6 +17,8 @@ namespace osu.Game.Storyboards public bool HasDrawable => Layers.Any(l => l.Elements.Any(e => e.IsDrawable)); + public double FirstEventTime => Layers.Min(l => l.Elements.FirstOrDefault()?.StartTime ?? 0); + public Storyboard() { layers.Add("Background", new StoryboardLayer("Background", 3)); diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index 0d9f4f51be..871d8ee3f1 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -5,25 +5,31 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; +using osu.Game.Storyboards; namespace osu.Game.Tests.Beatmaps { public class TestWorkingBeatmap : WorkingBeatmap { private readonly IBeatmap beatmap; + private readonly Storyboard storyboard; /// /// Create an instance which provides the when requested. /// - /// The beatmap - public TestWorkingBeatmap(IBeatmap beatmap) + /// The beatmap. + /// An optional storyboard. + public TestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) : base(beatmap.BeatmapInfo, null) { this.beatmap = beatmap; + this.storyboard = storyboard; } protected override IBeatmap GetBeatmap() => beatmap; + protected override Storyboard GetStoryboard() => storyboard ?? base.GetStoryboard(); + protected override Texture GetBackground() => null; protected override VideoSprite GetVideo() => null; diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 13e08c7d22..be67c2fe23 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -21,6 +21,7 @@ using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual @@ -119,10 +120,10 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); protected WorkingBeatmap CreateWorkingBeatmap(RulesetInfo ruleset) => - CreateWorkingBeatmap(CreateBeatmap(ruleset)); + CreateWorkingBeatmap(CreateBeatmap(ruleset), null); - protected virtual WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) => - new ClockBackedTestWorkingBeatmap(beatmap, Clock, audio); + protected virtual WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, Clock, audio); [BackgroundDependencyLoader] private void load(RulesetStore rulesets) @@ -168,7 +169,7 @@ namespace osu.Game.Tests.Visual /// A clock which should be used instead of a stopwatch for virtual time progression. /// Audio manager. Required if a reference clock isn't provided. public ClockBackedTestWorkingBeatmap(RulesetInfo ruleset, IFrameBasedClock referenceClock, AudioManager audio) - : this(new TestBeatmap(ruleset), referenceClock, audio) + : this(new TestBeatmap(ruleset), null, referenceClock, audio) { } @@ -176,11 +177,12 @@ namespace osu.Game.Tests.Visual /// Create an instance which provides the when requested. /// /// The beatmap + /// The storyboard. /// An optional clock which should be used instead of a stopwatch for virtual time progression. /// Audio manager. Required if a reference clock isn't provided. /// The length of the returned virtual track. - public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock referenceClock, AudioManager audio, double length = 60000) - : base(beatmap) + public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, double length = 60000) + : base(beatmap, storyboard) { if (referenceClock != null) {