From a5b3ac7ef8101c867bec8c1188ec4595ccb1c919 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Mar 2021 15:45:03 +0900 Subject: [PATCH 1/3] Add failing test covering alpha commands proceeding non-alpha (but ignored) commands --- .../Visual/Gameplay/TestSceneLeadIn.cs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 563d6be0da..dccde366c2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -46,11 +46,12 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(0, 0)] [TestCase(-1000, -1000)] [TestCase(-10000, -10000)] - public void TestStoryboardProducesCorrectStartTime(double firstStoryboardEvent, double expectedStartTime) + public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(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); @@ -64,6 +65,43 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [TestCase(1000, 0, false)] + [TestCase(0, 0, false)] + [TestCase(-1000, -1000, false)] + [TestCase(-10000, -10000, false)] + [TestCase(1000, 0, true)] + [TestCase(0, 0, true)] + [TestCase(-1000, -1000, true)] + [TestCase(-10000, -10000, true)] + public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop) + { + var storyboard = new Storyboard(); + + var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + + // these should be ignored as we have an alpha visibility blocker proceeding this command. + sprite.TimelineGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); + var loopGroup = sprite.AddLoop(-20000, 50); + loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); + + var target = addEventToLoop ? loopGroup : sprite.TimelineGroup; + target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); + + // these should be ignored due to being in the future. + sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); + loopGroup.Alpha.Add(Easing.None, 18000, 20000, 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", () => From 8aaba324314c4cfc628de35a3b6ab438227103a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Mar 2021 15:55:05 +0900 Subject: [PATCH 2/3] Fix storyboard commands occurring before the earliest point of visibility delaying gameplay In osu-stable, storyboard intros start from the first command, but in the case of storyboard drawables which have an initial hidden state, all commands before the time at which they become visible (ie. the first command where `Alpha` increases to a non-zero value) are ignored. This brings lazer in line with that behaviour. It also removes several unnecessary LINQ calls. Note that the alpha check being done in its own pass is important, as it must be the "minimum present alpha across all command groups, including loops". This is what makes the implementation slightly complex. Closes #11981. --- osu.Game/Storyboards/CommandTimelineGroup.cs | 19 +++++++++ osu.Game/Storyboards/StoryboardSprite.cs | 45 +++++++++++++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 6ce3b617e9..617455cf0b 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -45,11 +45,30 @@ namespace osu.Game.Storyboards }; } + /// + /// Returns the earliest visible time. Will be null unless this group has an command with a start value of zero. + /// + public double? EarliestDisplayedTime + { + get + { + var first = Alpha.Commands.FirstOrDefault(); + + return first?.StartValue == 0 ? first.StartTime : (double?)null; + } + } + [JsonIgnore] public double CommandsStartTime { get { + // if the first alpha command starts at zero it should be given priority over anything else. + // this is due to it creating a state where the target is not present before that time, causing any other events to not be visible. + var earliestDisplay = EarliestDisplayedTime; + if (earliestDisplay != null) + return earliestDisplay.Value; + double min = double.MaxValue; for (int i = 0; i < timelines.Length; i++) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index f411ad04f3..fdaa59d7d9 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -24,13 +24,46 @@ namespace osu.Game.Storyboards public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup(); - public double StartTime => Math.Min( - TimelineGroup.HasCommands ? TimelineGroup.CommandsStartTime : double.MaxValue, - loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Min(l => l.StartTime) : double.MaxValue); + public double StartTime + { + get + { + // check for presence affecting commands as an initial pass. + double earliestStartTime = TimelineGroup.EarliestDisplayedTime ?? double.MaxValue; - public double EndTime => Math.Max( - TimelineGroup.HasCommands ? TimelineGroup.CommandsEndTime : double.MinValue, - loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Max(l => l.EndTime) : double.MinValue); + foreach (var l in loops) + { + if (!(l.EarliestDisplayedTime is double lEarliest)) + continue; + + earliestStartTime = Math.Min(earliestStartTime, lEarliest); + } + + if (earliestStartTime < double.MaxValue) + return earliestStartTime; + + // if an alpha-affecting command was not found, use the earliest of any command. + earliestStartTime = TimelineGroup.StartTime; + + foreach (var l in loops) + earliestStartTime = Math.Min(earliestStartTime, l.StartTime); + + return earliestStartTime; + } + } + + public double EndTime + { + get + { + double latestEndTime = TimelineGroup.EndTime; + + foreach (var l in loops) + latestEndTime = Math.Max(latestEndTime, l.EndTime); + + return latestEndTime; + } + } public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands); From efb4a366d42600b5217e574f40c5d53438a8d3c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Mar 2021 12:15:59 +0900 Subject: [PATCH 3/3] Fix xmldoc explaining incorrect behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Storyboards/CommandTimelineGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 617455cf0b..c478b91c22 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -46,7 +46,7 @@ namespace osu.Game.Storyboards } /// - /// Returns the earliest visible time. Will be null unless this group has an command with a start value of zero. + /// Returns the earliest visible time. Will be null unless this group's first command has a start value of zero. /// public double? EarliestDisplayedTime {