diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index bcde899789..560e2ef894 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -149,5 +149,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType); } } + + [Test] + public void TestDecodeLoopCount() + { + // all loop sequences in loop-count.osb have a total duration of 2000ms (fade in 0->1000ms, fade out 1000->2000ms). + const double loop_duration = 2000; + + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("loop-count.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + + // stable ensures that any loop command executes at least once, even if the loop count specified in the .osb is zero or negative. + StoryboardSprite zeroTimes = background.Elements.OfType().Single(s => s.Path == "zero-times.png"); + Assert.That(zeroTimes.EndTime, Is.EqualTo(1000 + loop_duration)); + + StoryboardSprite oneTime = background.Elements.OfType().Single(s => s.Path == "one-time.png"); + Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration)); + + StoryboardSprite manyTimes = background.Elements.OfType().Single(s => s.Path == "many-times.png"); + Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration)); + } + } } } diff --git a/osu.Game.Tests/Resources/loop-count.osb b/osu.Game.Tests/Resources/loop-count.osb new file mode 100644 index 0000000000..ec75e85ef1 --- /dev/null +++ b/osu.Game.Tests/Resources/loop-count.osb @@ -0,0 +1,15 @@ +osu file format v14 + +[Events] +Sprite,Background,TopCentre,"zero-times.png",320,240 + L,1000,0 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 +Sprite,Background,TopCentre,"one-time.png",320,240 + L,4000,1 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 +Sprite,Background,TopCentre,"many-times.png",320,240 + L,9000,40 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 6301c42deb..0f15e28c00 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -176,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats case "L": { var startTime = Parsing.ParseDouble(split[1]); - var loopCount = Parsing.ParseInt(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); + var repeatCount = Parsing.ParseInt(split[2]); + timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1)); break; } diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index c22ca0d8c0..66db965803 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.cs @@ -1,6 +1,7 @@ // 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; namespace osu.Game.Storyboards @@ -8,20 +9,31 @@ namespace osu.Game.Storyboards public class CommandLoop : CommandTimelineGroup { public double LoopStartTime; - public int LoopCount; + + /// + /// The total number of times this loop is played back. Always greater than zero. + /// + public readonly int TotalIterations; public override double StartTime => LoopStartTime + CommandsStartTime; - public override double EndTime => StartTime + CommandsDuration * LoopCount; + public override double EndTime => StartTime + CommandsDuration * TotalIterations; - public CommandLoop(double startTime, int loopCount) + /// + /// Construct a new command loop. + /// + /// The start time of the loop. + /// The number of times the loop should repeat. Should be greater than zero. Zero means a single playback. + public CommandLoop(double startTime, int repeatCount) { + if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount)); + LoopStartTime = startTime; - LoopCount = loopCount; + TotalIterations = repeatCount + 1; } public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) { - for (var loop = 0; loop < LoopCount; loop++) + for (var loop = 0; loop < TotalIterations; loop++) { var loopOffset = LoopStartTime + loop * CommandsDuration; foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset)) @@ -30,6 +42,6 @@ namespace osu.Game.Storyboards } public override string ToString() - => $"{LoopStartTime} x{LoopCount}"; + => $"{LoopStartTime} x{TotalIterations}"; } } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index bf87e7d10e..6fb2f5994b 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -1,13 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK; -using osu.Framework.Graphics; -using osu.Game.Storyboards.Drawables; using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Game.Storyboards.Drawables; +using osuTK; namespace osu.Game.Storyboards { @@ -78,9 +78,9 @@ namespace osu.Game.Storyboards InitialPosition = initialPosition; } - public CommandLoop AddLoop(double startTime, int loopCount) + public CommandLoop AddLoop(double startTime, int repeatCount) { - var loop = new CommandLoop(startTime, loopCount); + var loop = new CommandLoop(startTime, repeatCount); loops.Add(loop); return loop; }