From d6d8ea5b6b5ce956d81863136f5eff3f62bf5cd6 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 12 Apr 2021 11:17:56 +0900 Subject: [PATCH 01/11] Throw when getting a frame of an empty replay --- .../Gameplay/TestSceneSpectatorPlayback.cs | 30 +++++++++---------- osu.Game/Input/Handlers/ReplayInputHandler.cs | 2 -- .../Replays/FramedReplayInputHandler.cs | 9 ++++-- osu.Game/Rulesets/UI/RulesetInputManager.cs | 10 +++++++ 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 35b3bfc1f8..9c763814f3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -204,27 +204,27 @@ namespace osu.Game.Tests.Visual.Gameplay return; } - if (replayHandler.NextFrame != null) - { - var lastFrame = replay.Frames.LastOrDefault(); + if (!replayHandler.HasFrames) + return; - // this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved). - // in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation. - if (lastFrame != null) - latency = Math.Max(latency, Time.Current - lastFrame.Time); + var lastFrame = replay.Frames.LastOrDefault(); - latencyDisplay.Text = $"latency: {latency:N1}"; + // this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved). + // in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation. + if (lastFrame != null) + latency = Math.Max(latency, Time.Current - lastFrame.Time); - double proposedTime = Time.Current - latency + Time.Elapsed; + latencyDisplay.Text = $"latency: {latency:N1}"; - // this will either advance by one or zero frames. - double? time = replayHandler.SetFrameFromTime(proposedTime); + double proposedTime = Time.Current - latency + Time.Elapsed; - if (time == null) - return; + // this will either advance by one or zero frames. + double? time = replayHandler.SetFrameFromTime(proposedTime); - manualClock.CurrentTime = time.Value; - } + if (time == null) + return; + + manualClock.CurrentTime = time.Value; } [TearDownSteps] diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs index fba1bee0b8..cd76000f98 100644 --- a/osu.Game/Input/Handlers/ReplayInputHandler.cs +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -32,8 +32,6 @@ namespace osu.Game.Input.Handlers public override bool Initialize(GameHost host) => true; - public override bool IsActive => true; - public class ReplayState : IInput where T : struct { diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 0b41ca31ea..5eaccc766e 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Replays public abstract class FramedReplayInputHandler : ReplayInputHandler where TFrame : ReplayFrame { + public override bool IsActive => HasFrames; + private readonly Replay replay; protected List Frames => replay.Frames; @@ -25,7 +27,10 @@ namespace osu.Game.Rulesets.Replays { get { - if (!HasFrames || !currentFrameIndex.HasValue) + if (!HasFrames) + throw new InvalidOperationException($"Cannot get {nameof(CurrentFrame)} of the empty replay"); + + if (!currentFrameIndex.HasValue) return null; return (TFrame)Frames[currentFrameIndex.Value]; @@ -37,7 +42,7 @@ namespace osu.Game.Rulesets.Replays get { if (!HasFrames) - return null; + throw new InvalidOperationException($"Cannot get {nameof(NextFrame)} of the empty replay"); if (!currentFrameIndex.HasValue) return currentDirection > 0 ? (TFrame)Frames[0] : null; diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index d6f002ea2c..fb56a5d93d 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.States; using osu.Game.Configuration; @@ -100,6 +102,14 @@ namespace osu.Game.Rulesets.UI #endregion + protected override List GetPendingInputs() + { + if (replayInputHandler != null && !replayInputHandler.IsActive) + return new List(); + + return base.GetPendingInputs(); + } + #region Setting application (disables etc.) private Bindable mouseDisabled; From e19e8ff2a3252b9adde46dae589ce8d2a134f4dc Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 12 Apr 2021 15:52:43 +0900 Subject: [PATCH 02/11] Rewrite FramedReplayInputHandler for robustness This commit changes the semantics of `CurrentFrame` and `NextFrame` of the class. The ordering of `NextFrame.Time` and `CurrentFrame.Time` was dependent on the current direction. Now, it should always satisfy `CurrentFrame.Time <= CurrentTime <= NextFrame.Time` except at the start/end. This change, however, doesn't break existing deriving classes if the template code pattern usage of interpolation is used. The deriving class code can be simplified due to the elimination of nullable types. I didn't include those changes in this commit. I removed `StreamingFramedReplayInputHandlerTest` for now, as it is almost-duplicate of `FramedReplayInputHandlerTest`. I'll include more tests in later commits. This commit fixes #6150. --- .../NonVisual/FramedReplayInputHandlerTest.cs | 81 ++--- .../StreamingFramedReplayInputHandlerTest.cs | 296 ------------------ .../Visual/Gameplay/TestSceneSpectator.cs | 4 +- .../Replays/FramedReplayInputHandler.cs | 167 +++++----- 4 files changed, 109 insertions(+), 439 deletions(-) delete mode 100644 osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs index 92a60663de..b4fc081a2a 100644 --- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs +++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs @@ -37,11 +37,6 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestNormalPlayback() { - Assert.IsNull(handler.CurrentFrame); - - confirmCurrentFrame(null); - confirmNextFrame(0); - setTime(0, 0); confirmCurrentFrame(0); confirmNextFrame(1); @@ -97,22 +92,22 @@ namespace osu.Game.Tests.NonVisual // exited important section setTime(8200, 8000); confirmCurrentFrame(7); - confirmNextFrame(null); + confirmNextFrame(7); setTime(8200, 8200); confirmCurrentFrame(7); - confirmNextFrame(null); + confirmNextFrame(7); } [Test] public void TestIntroTime() { setTime(-1000, -1000); - confirmCurrentFrame(null); + confirmCurrentFrame(0); confirmNextFrame(0); setTime(-500, -500); - confirmCurrentFrame(null); + confirmCurrentFrame(0); confirmNextFrame(0); setTime(0, 0); @@ -133,29 +128,29 @@ namespace osu.Game.Tests.NonVisual // pivot without crossing a frame boundary setTime(2700, 2700); confirmCurrentFrame(2); - confirmNextFrame(1); + confirmNextFrame(3); - // cross current frame boundary; should not yet update frame - setTime(1980, 1980); + // cross current frame boundary + setTime(1980, 2000); confirmCurrentFrame(2); - confirmNextFrame(1); + confirmNextFrame(3); setTime(1200, 1200); - confirmCurrentFrame(2); - confirmNextFrame(1); + confirmCurrentFrame(1); + confirmNextFrame(2); // ensure each frame plays out until start setTime(-500, 1000); confirmCurrentFrame(1); - confirmNextFrame(0); + confirmNextFrame(2); setTime(-500, 0); confirmCurrentFrame(0); - confirmNextFrame(null); + confirmNextFrame(1); setTime(-500, -500); confirmCurrentFrame(0); - confirmNextFrame(null); + confirmNextFrame(0); } [Test] @@ -168,12 +163,12 @@ namespace osu.Game.Tests.NonVisual confirmNextFrame(5); setTime(3500, null); - confirmCurrentFrame(4); - confirmNextFrame(3); + confirmCurrentFrame(3); + confirmNextFrame(4); setTime(3000, 3000); confirmCurrentFrame(3); - confirmNextFrame(2); + confirmNextFrame(4); setTime(3500, null); confirmCurrentFrame(3); @@ -187,17 +182,17 @@ namespace osu.Game.Tests.NonVisual confirmCurrentFrame(4); confirmNextFrame(5); - setTime(4000, null); + setTime(4000, 4000); confirmCurrentFrame(4); confirmNextFrame(5); setTime(3500, null); - confirmCurrentFrame(4); - confirmNextFrame(3); + confirmCurrentFrame(3); + confirmNextFrame(4); setTime(3000, 3000); confirmCurrentFrame(3); - confirmNextFrame(2); + confirmNextFrame(4); } [Test] @@ -209,24 +204,24 @@ namespace osu.Game.Tests.NonVisual confirmNextFrame(4); setTime(3200, null); - // next frame doesn't change even though direction reversed, because of important section. confirmCurrentFrame(3); confirmNextFrame(4); - setTime(3000, null); + setTime(3000, 3000); confirmCurrentFrame(3); confirmNextFrame(4); setTime(2800, 2800); - confirmCurrentFrame(3); - confirmNextFrame(2); + confirmCurrentFrame(2); + confirmNextFrame(3); } private void fastForwardToPoint(double destination) { for (int i = 0; i < 1000; i++) { - if (handler.SetFrameFromTime(destination) == null) + var time = handler.SetFrameFromTime(destination); + if (time == null || time == destination) return; } @@ -235,33 +230,17 @@ namespace osu.Game.Tests.NonVisual private void setTime(double set, double? expect) { - Assert.AreEqual(expect, handler.SetFrameFromTime(set)); + Assert.AreEqual(expect, handler.SetFrameFromTime(set), "Unexpected return value"); } - private void confirmCurrentFrame(int? frame) + private void confirmCurrentFrame(int frame) { - if (frame.HasValue) - { - Assert.IsNotNull(handler.CurrentFrame); - Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time); - } - else - { - Assert.IsNull(handler.CurrentFrame); - } + Assert.AreEqual(replay.Frames[frame].Time, handler.CurrentFrame.Time, "Unexpected current frame"); } - private void confirmNextFrame(int? frame) + private void confirmNextFrame(int frame) { - if (frame.HasValue) - { - Assert.IsNotNull(handler.NextFrame); - Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time); - } - else - { - Assert.IsNull(handler.NextFrame); - } + Assert.AreEqual(replay.Frames[frame].Time, handler.NextFrame.Time, "Unexpected next frame"); } private class TestReplayFrame : ReplayFrame diff --git a/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs deleted file mode 100644 index 21ec29b10b..0000000000 --- a/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs +++ /dev/null @@ -1,296 +0,0 @@ -// 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 NUnit.Framework; -using osu.Game.Replays; -using osu.Game.Rulesets.Replays; - -namespace osu.Game.Tests.NonVisual -{ - [TestFixture] - public class StreamingFramedReplayInputHandlerTest - { - private Replay replay; - private TestInputHandler handler; - - [SetUp] - public void SetUp() - { - handler = new TestInputHandler(replay = new Replay - { - HasReceivedAllFrames = false, - Frames = new List - { - new TestReplayFrame(0), - new TestReplayFrame(1000), - new TestReplayFrame(2000), - new TestReplayFrame(3000, true), - new TestReplayFrame(4000, true), - new TestReplayFrame(5000, true), - new TestReplayFrame(7000, true), - new TestReplayFrame(8000), - } - }); - } - - [Test] - public void TestNormalPlayback() - { - Assert.IsNull(handler.CurrentFrame); - - confirmCurrentFrame(null); - confirmNextFrame(0); - - setTime(0, 0); - confirmCurrentFrame(0); - confirmNextFrame(1); - - // if we hit the first frame perfectly, time should progress to it. - setTime(1000, 1000); - confirmCurrentFrame(1); - confirmNextFrame(2); - - // in between non-important frames should progress based on input. - setTime(1200, 1200); - confirmCurrentFrame(1); - - setTime(1400, 1400); - confirmCurrentFrame(1); - - // progressing beyond the next frame should force time to that frame once. - setTime(2200, 2000); - confirmCurrentFrame(2); - - // second attempt should progress to input time - setTime(2200, 2200); - confirmCurrentFrame(2); - - // entering important section - setTime(3000, 3000); - confirmCurrentFrame(3); - - // cannot progress within - setTime(3500, null); - confirmCurrentFrame(3); - - setTime(4000, 4000); - confirmCurrentFrame(4); - - // still cannot progress - setTime(4500, null); - confirmCurrentFrame(4); - - setTime(5200, 5000); - confirmCurrentFrame(5); - - // important section AllowedImportantTimeSpan allowance - setTime(5200, 5200); - confirmCurrentFrame(5); - - setTime(7200, 7000); - confirmCurrentFrame(6); - - setTime(7200, null); - confirmCurrentFrame(6); - - // exited important section - setTime(8200, 8000); - confirmCurrentFrame(7); - confirmNextFrame(null); - - setTime(8200, null); - confirmCurrentFrame(7); - confirmNextFrame(null); - - setTime(8400, null); - confirmCurrentFrame(7); - confirmNextFrame(null); - } - - [Test] - public void TestIntroTime() - { - setTime(-1000, -1000); - confirmCurrentFrame(null); - confirmNextFrame(0); - - setTime(-500, -500); - confirmCurrentFrame(null); - confirmNextFrame(0); - - setTime(0, 0); - confirmCurrentFrame(0); - confirmNextFrame(1); - } - - [Test] - public void TestBasicRewind() - { - setTime(2800, 0); - setTime(2800, 1000); - setTime(2800, 2000); - setTime(2800, 2800); - confirmCurrentFrame(2); - confirmNextFrame(3); - - // pivot without crossing a frame boundary - setTime(2700, 2700); - confirmCurrentFrame(2); - confirmNextFrame(1); - - // cross current frame boundary; should not yet update frame - setTime(1980, 1980); - confirmCurrentFrame(2); - confirmNextFrame(1); - - setTime(1200, 1200); - confirmCurrentFrame(2); - confirmNextFrame(1); - - // ensure each frame plays out until start - setTime(-500, 1000); - confirmCurrentFrame(1); - confirmNextFrame(0); - - setTime(-500, 0); - confirmCurrentFrame(0); - confirmNextFrame(null); - - setTime(-500, -500); - confirmCurrentFrame(0); - confirmNextFrame(null); - } - - [Test] - public void TestRewindInsideImportantSection() - { - fastForwardToPoint(3000); - - setTime(4000, 4000); - confirmCurrentFrame(4); - confirmNextFrame(5); - - setTime(3500, null); - confirmCurrentFrame(4); - confirmNextFrame(3); - - setTime(3000, 3000); - confirmCurrentFrame(3); - confirmNextFrame(2); - - setTime(3500, null); - confirmCurrentFrame(3); - confirmNextFrame(4); - - setTime(4000, 4000); - confirmCurrentFrame(4); - confirmNextFrame(5); - - setTime(4500, null); - confirmCurrentFrame(4); - confirmNextFrame(5); - - setTime(4000, null); - confirmCurrentFrame(4); - confirmNextFrame(5); - - setTime(3500, null); - confirmCurrentFrame(4); - confirmNextFrame(3); - - setTime(3000, 3000); - confirmCurrentFrame(3); - confirmNextFrame(2); - } - - [Test] - public void TestRewindOutOfImportantSection() - { - fastForwardToPoint(3500); - - confirmCurrentFrame(3); - confirmNextFrame(4); - - setTime(3200, null); - // next frame doesn't change even though direction reversed, because of important section. - confirmCurrentFrame(3); - confirmNextFrame(4); - - setTime(3000, null); - confirmCurrentFrame(3); - confirmNextFrame(4); - - setTime(2800, 2800); - confirmCurrentFrame(3); - confirmNextFrame(2); - } - - private void fastForwardToPoint(double destination) - { - for (int i = 0; i < 1000; i++) - { - if (handler.SetFrameFromTime(destination) == null) - return; - } - - throw new TimeoutException("Seek was never fulfilled"); - } - - private void setTime(double set, double? expect) - { - Assert.AreEqual(expect, handler.SetFrameFromTime(set)); - } - - private void confirmCurrentFrame(int? frame) - { - if (frame.HasValue) - { - Assert.IsNotNull(handler.CurrentFrame); - Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time); - } - else - { - Assert.IsNull(handler.CurrentFrame); - } - } - - private void confirmNextFrame(int? frame) - { - if (frame.HasValue) - { - Assert.IsNotNull(handler.NextFrame); - Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time); - } - else - { - Assert.IsNull(handler.NextFrame); - } - } - - private class TestReplayFrame : ReplayFrame - { - public readonly bool IsImportant; - - public TestReplayFrame(double time, bool isImportant = false) - : base(time) - { - IsImportant = isImportant; - } - } - - private class TestInputHandler : FramedReplayInputHandler - { - public TestInputHandler(Replay replay) - : base(replay) - { - FrameAccuratePlayback = true; - } - - protected override double AllowedImportantTimeSpan => 1000; - - protected override bool IsImportant(TestReplayFrame frame) => frame.IsImportant; - } - } -} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 9d85a9995d..9f1faa8e26 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); - AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); + AddUntilStep("wait for frame starvation", () => replayHandler.WaitingNextFrame); checkPaused(true); double? pausedTime = null; @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); - AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); + AddUntilStep("wait for frame starvation", () => replayHandler.WaitingNextFrame); checkPaused(true); AddAssert("time advanced", () => currentFrameStableTime > pausedTime); diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 5eaccc766e..f527c0e105 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -1,9 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; -using System.Diagnostics; using JetBrains.Annotations; using osu.Game.Input.Handlers; using osu.Game.Replays; @@ -17,70 +18,80 @@ namespace osu.Game.Rulesets.Replays public abstract class FramedReplayInputHandler : ReplayInputHandler where TFrame : ReplayFrame { - public override bool IsActive => HasFrames; + /// + /// Whether we have at least one replay frame. + /// + public bool HasFrames => Frames.Count != 0; - private readonly Replay replay; - - protected List Frames => replay.Frames; + /// + /// Whether we are waiting for new frames to be received. + /// + public bool WaitingNextFrame => !replay.HasReceivedAllFrames && currentFrameIndex == Frames.Count - 1; + /// + /// The current frame of the replay. + /// The current time is always between the start and the end time of the current frame. + /// + /// The replay is empty. public TFrame CurrentFrame { get { if (!HasFrames) - throw new InvalidOperationException($"Cannot get {nameof(CurrentFrame)} of the empty replay"); + throw new InvalidOperationException($"Attempted to get {nameof(CurrentFrame)} of an empty replay"); - if (!currentFrameIndex.HasValue) - return null; - - return (TFrame)Frames[currentFrameIndex.Value]; + return (TFrame)Frames[Math.Max(0, currentFrameIndex)]; } } + /// + /// The next frame of the replay. + /// The start time is always greater or equals to the start time of regardless of the seeking direction. + /// If it is before the first frame of the replay or the after the last frame of the replay, and agree. + /// + /// The replay is empty. public TFrame NextFrame { get { if (!HasFrames) - throw new InvalidOperationException($"Cannot get {nameof(NextFrame)} of the empty replay"); + throw new InvalidOperationException($"Attempted to get {nameof(NextFrame)} of an empty replay"); - if (!currentFrameIndex.HasValue) - return currentDirection > 0 ? (TFrame)Frames[0] : null; - - int nextFrame = clampedNextFrameIndex; - - if (nextFrame == currentFrameIndex.Value) - return null; - - return (TFrame)Frames[clampedNextFrameIndex]; + return (TFrame)Frames[Math.Min(currentFrameIndex + 1, Frames.Count - 1)]; } } - private int? currentFrameIndex; - - private int clampedNextFrameIndex => - currentFrameIndex.HasValue ? Math.Clamp(currentFrameIndex.Value + currentDirection, 0, Frames.Count - 1) : 0; - - protected FramedReplayInputHandler(Replay replay) - { - this.replay = replay; - } - - private const double sixty_frame_time = 1000.0 / 60; - - protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2; - - protected double? CurrentTime { get; private set; } - - private int currentDirection = 1; - /// /// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data. /// Disabling this can make replay playback smoother (useful for autoplay, currently). /// public bool FrameAccuratePlayback; - public bool HasFrames => Frames.Count > 0; + // This input handler should be enabled only if there is at least one replay frame. + public override bool IsActive => HasFrames; + + // Can make it non-null but that is a breaking change. + protected double? CurrentTime { get; private set; } + + protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2; + + protected List Frames => replay.Frames; + + private readonly Replay replay; + + private int currentFrameIndex; + + private const double sixty_frame_time = 1000.0 / 60; + + protected FramedReplayInputHandler(Replay replay) + { + // This replay frame ordering should be enforced on the Replay type + replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time)); + + this.replay = replay; + currentFrameIndex = -1; + CurrentTime = double.NegativeInfinity; + } private bool inImportantSection { @@ -89,13 +100,8 @@ namespace osu.Game.Rulesets.Replays if (!HasFrames || !FrameAccuratePlayback) return false; - var frame = currentDirection > 0 ? CurrentFrame : NextFrame; - - if (frame == null) - return false; - - return IsImportant(frame) && // a button is in a pressed state - Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span + return IsImportant(CurrentFrame) && // a button is in a pressed state + Math.Abs(CurrentTime - NextFrame.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span } } @@ -110,71 +116,52 @@ namespace osu.Game.Rulesets.Replays /// The usable time value. If null, we should not advance time as we do not have enough data. public override double? SetFrameFromTime(double time) { - updateDirection(time); - - Debug.Assert(currentDirection != 0); - if (!HasFrames) { - // in the case all frames are received, allow time to progress regardless. + // In the case all frames are received, allow time to progress regardless. if (replay.HasReceivedAllFrames) return CurrentTime = time; return null; } - TFrame next = NextFrame; + double frameStart = getFrameTime(currentFrameIndex); + double frameEnd = getFrameTime(currentFrameIndex + 1); - // if we have a next frame, check if it is before or at the current time in playback, and advance time to it if so. - if (next != null) + // If the proposed time is after the current frame end time, we progress forwards. + // If the proposed time is before the current frame start time, and we are at the frame boundary, we progress backwards. + if (frameEnd <= time) { - int compare = time.CompareTo(next.Time); - - if (compare == 0 || compare == currentDirection) - { - currentFrameIndex = clampedNextFrameIndex; - return CurrentTime = CurrentFrame.Time; - } + time = frameEnd; + currentFrameIndex++; } + else if (time < frameStart && CurrentTime == frameStart) + currentFrameIndex--; - // at this point, the frame index can't be advanced. - // even so, we may be able to propose the clock progresses forward due to being at an extent of the replay, - // or moving towards the next valid frame (ie. interpolating in a non-important section). + frameStart = getFrameTime(currentFrameIndex); + frameEnd = getFrameTime(currentFrameIndex + 1); - // the exception is if currently in an important section, which is respected above all. - if (inImportantSection) + // Pause until more frames are arrived. + if (WaitingNextFrame && frameStart < time) { - Debug.Assert(next != null || !replay.HasReceivedAllFrames); + CurrentTime = frameStart; return null; } - // if a next frame does exist, allow interpolation. - if (next != null) - return CurrentTime = time; + CurrentTime = Math.Clamp(time, frameStart, frameEnd); - // if all frames have been received, allow playing beyond extents. - if (replay.HasReceivedAllFrames) - return CurrentTime = time; - - // if not all frames are received but we are before the first frame, allow playing. - if (time < Frames[0].Time) - return CurrentTime = time; - - // in the case we have no next frames and haven't received enough frame data, block. - return null; + // In an important section, a mid-frame time cannot be used and a null is returned instead. + return inImportantSection && frameStart < time && time < frameEnd ? null : CurrentTime; } - private void updateDirection(double time) + private double getFrameTime(int index) { - if (!CurrentTime.HasValue) - { - currentDirection = 1; - } - else - { - currentDirection = time.CompareTo(CurrentTime); - if (currentDirection == 0) currentDirection = 1; - } + if (index < 0) + return double.NegativeInfinity; + if (index >= Frames.Count) + return double.PositiveInfinity; + + return Frames[index].Time; } } } From 3c28c09ab5073c20a8eaebb8ce043a6cdc16b77f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 12 Apr 2021 16:18:35 +0900 Subject: [PATCH 03/11] Add more FramedReplayInputHandler tests --- .../NonVisual/FramedReplayInputHandlerTest.cs | 100 ++++++++++++++++-- 1 file changed, 89 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs index b4fc081a2a..64af4b6db6 100644 --- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs +++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs @@ -20,23 +20,15 @@ namespace osu.Game.Tests.NonVisual { handler = new TestInputHandler(replay = new Replay { - Frames = new List - { - new TestReplayFrame(0), - new TestReplayFrame(1000), - new TestReplayFrame(2000), - new TestReplayFrame(3000, true), - new TestReplayFrame(4000, true), - new TestReplayFrame(5000, true), - new TestReplayFrame(7000, true), - new TestReplayFrame(8000), - } + HasReceivedAllFrames = false }); } [Test] public void TestNormalPlayback() { + setReplayFrames(); + setTime(0, 0); confirmCurrentFrame(0); confirmNextFrame(1); @@ -102,6 +94,8 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestIntroTime() { + setReplayFrames(); + setTime(-1000, -1000); confirmCurrentFrame(0); confirmNextFrame(0); @@ -118,6 +112,8 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestBasicRewind() { + setReplayFrames(); + setTime(2800, 0); setTime(2800, 1000); setTime(2800, 2000); @@ -156,6 +152,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestRewindInsideImportantSection() { + setReplayFrames(); fastForwardToPoint(3000); setTime(4000, 4000); @@ -198,6 +195,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestRewindOutOfImportantSection() { + setReplayFrames(); fastForwardToPoint(3500); confirmCurrentFrame(3); @@ -216,6 +214,86 @@ namespace osu.Game.Tests.NonVisual confirmNextFrame(3); } + [Test] + public void TestReplayStreaming() + { + // no frames are arrived yet + setTime(0, null); + setTime(1000, null); + Assert.IsTrue(handler.WaitingNextFrame, "Should be waiting for the first frame"); + + replay.Frames.Add(new TestReplayFrame(0)); + replay.Frames.Add(new TestReplayFrame(1000)); + + // should always play from beginning + setTime(1000, 0); + confirmCurrentFrame(0); + Assert.IsFalse(handler.WaitingNextFrame, "Should not be waiting yet"); + setTime(1000, 1000); + confirmCurrentFrame(1); + confirmNextFrame(1); + Assert.IsTrue(handler.WaitingNextFrame, "Should be waiting"); + + // cannot seek beyond the last frame + setTime(1500, null); + confirmCurrentFrame(1); + + setTime(-100, 0); + confirmCurrentFrame(0); + + // can seek to the point before the first frame, however + setTime(-100, -100); + confirmCurrentFrame(0); + confirmNextFrame(0); + + fastForwardToPoint(1000); + setTime(3000, null); + replay.Frames.Add(new TestReplayFrame(2000)); + confirmCurrentFrame(1); + setTime(1000, 1000); + setTime(3000, 2000); + } + + [Test] + public void TestMultipleFramesSameTime() + { + replay.Frames.Add(new TestReplayFrame(0)); + replay.Frames.Add(new TestReplayFrame(0)); + replay.Frames.Add(new TestReplayFrame(1000)); + replay.Frames.Add(new TestReplayFrame(1000)); + replay.Frames.Add(new TestReplayFrame(2000)); + + // forward direction is prioritized when multiple frames have the same time. + setTime(0, 0); + setTime(0, 0); + + setTime(2000, 1000); + setTime(2000, 1000); + + setTime(1000, 1000); + setTime(1000, 1000); + setTime(-100, 1000); + setTime(-100, 0); + setTime(-100, 0); + setTime(-100, -100); + } + + private void setReplayFrames() + { + replay.Frames = new List + { + new TestReplayFrame(0), + new TestReplayFrame(1000), + new TestReplayFrame(2000), + new TestReplayFrame(3000, true), + new TestReplayFrame(4000, true), + new TestReplayFrame(5000, true), + new TestReplayFrame(7000, true), + new TestReplayFrame(8000), + }; + replay.HasReceivedAllFrames = true; + } + private void fastForwardToPoint(double destination) { for (int i = 0; i < 1000; i++) From f1b8171e389e5df4eb67b567d4e48586a3001c73 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 12 Apr 2021 17:13:48 +0900 Subject: [PATCH 04/11] Remove `#nullable true` for now to suppress inspector --- osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index f527c0e105..45bcd0bc19 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable enable - using System; using System.Collections.Generic; using JetBrains.Annotations; From 6d18b3db00a5f3f045dc35cdc6a740bae7156f90 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 12 Apr 2021 18:49:38 +0900 Subject: [PATCH 05/11] Avoid empty list allocation --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index fb56a5d93d..1c0d820a3d 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -102,10 +102,13 @@ namespace osu.Game.Rulesets.UI #endregion + // to avoid allocation + private readonly List emptyInputList = new List(); + protected override List GetPendingInputs() { if (replayInputHandler != null && !replayInputHandler.IsActive) - return new List(); + return emptyInputList; return base.GetPendingInputs(); } From 359fae895f843ff7b8775917d443a89556705d07 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 12 Apr 2021 18:50:25 +0900 Subject: [PATCH 06/11] Rename property --- osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs | 6 +++--- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 4 ++-- osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs index 64af4b6db6..954871595e 100644 --- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs +++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs @@ -220,7 +220,7 @@ namespace osu.Game.Tests.NonVisual // no frames are arrived yet setTime(0, null); setTime(1000, null); - Assert.IsTrue(handler.WaitingNextFrame, "Should be waiting for the first frame"); + Assert.IsTrue(handler.WaitingForFrame, "Should be waiting for the first frame"); replay.Frames.Add(new TestReplayFrame(0)); replay.Frames.Add(new TestReplayFrame(1000)); @@ -228,11 +228,11 @@ namespace osu.Game.Tests.NonVisual // should always play from beginning setTime(1000, 0); confirmCurrentFrame(0); - Assert.IsFalse(handler.WaitingNextFrame, "Should not be waiting yet"); + Assert.IsFalse(handler.WaitingForFrame, "Should not be waiting yet"); setTime(1000, 1000); confirmCurrentFrame(1); confirmNextFrame(1); - Assert.IsTrue(handler.WaitingNextFrame, "Should be waiting"); + Assert.IsTrue(handler.WaitingForFrame, "Should be waiting"); // cannot seek beyond the last frame setTime(1500, null); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 9f1faa8e26..397b37718d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); - AddUntilStep("wait for frame starvation", () => replayHandler.WaitingNextFrame); + AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame); checkPaused(true); double? pausedTime = null; @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); - AddUntilStep("wait for frame starvation", () => replayHandler.WaitingNextFrame); + AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame); checkPaused(true); AddAssert("time advanced", () => currentFrameStableTime > pausedTime); diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 45bcd0bc19..23cc311d79 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Replays /// /// Whether we are waiting for new frames to be received. /// - public bool WaitingNextFrame => !replay.HasReceivedAllFrames && currentFrameIndex == Frames.Count - 1; + public bool WaitingForFrame => !replay.HasReceivedAllFrames && currentFrameIndex == Frames.Count - 1; /// /// The current frame of the replay. @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Replays frameEnd = getFrameTime(currentFrameIndex + 1); // Pause until more frames are arrived. - if (WaitingNextFrame && frameStart < time) + if (WaitingForFrame && frameStart < time) { CurrentTime = frameStart; return null; From 31d36071052bf72f9a82256242cbb6fa7c2b0e38 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 12 Apr 2021 18:50:54 +0900 Subject: [PATCH 07/11] Add TODO comment --- osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 23cc311d79..c3cd957f0d 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Replays /// /// The next frame of the replay. - /// The start time is always greater or equals to the start time of regardless of the seeking direction. + /// The start time is always greater or equal to the start time of regardless of the seeking direction. /// If it is before the first frame of the replay or the after the last frame of the replay, and agree. /// /// The replay is empty. @@ -83,7 +83,8 @@ namespace osu.Game.Rulesets.Replays protected FramedReplayInputHandler(Replay replay) { - // This replay frame ordering should be enforced on the Replay type + // TODO: This replay frame ordering should be enforced on the Replay type. + // Currently, the ordering can be broken if the frames are added after this construction. replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time)); this.replay = replay; From d0f30b7b422afe385fd1fc0de7a28684bf0ab7ef Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 13 Apr 2021 14:29:47 +0900 Subject: [PATCH 08/11] Delay map completion one frame after the last judgment This is a workaround of a timing issue. KeyCounter is disabled while break time (`HasCompleted == true`). When the last keypress is exactly at the same time the map ends, the last frame was considered in a break time while forward play but considered not in a break time while rewinding. This inconsistency made the last keypress not decremented in the key counter when a replay is rewound. The situation regularly happens in osu!standard because the map ends right after the player hits the last hit circle. It was caught by `TestSceneGameplayRewinding`. This commit makes the update of the map completion delayed one frame. The problematic keypress frame is now processed strictly before the map completion, and the map completion status is correctly rewound before the keypress frame. --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 8aef615b5f..201a05e569 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Scoring /// public int JudgedHits { get; private set; } + private JudgementResult lastAppliedResult; + private readonly BindableBool hasCompleted = new BindableBool(); /// @@ -53,12 +55,11 @@ namespace osu.Game.Rulesets.Scoring public void ApplyResult(JudgementResult result) { JudgedHits++; + lastAppliedResult = result; ApplyResultInternal(result); NewJudgement?.Invoke(result); - - updateHasCompleted(); } /// @@ -69,8 +70,6 @@ namespace osu.Game.Rulesets.Scoring { JudgedHits--; - updateHasCompleted(); - RevertResultInternal(result); } @@ -134,6 +133,10 @@ namespace osu.Game.Rulesets.Scoring } } - private void updateHasCompleted() => hasCompleted.Value = JudgedHits == MaxHits; + protected override void Update() + { + base.Update(); + hasCompleted.Value = JudgedHits == MaxHits && (JudgedHits == 0 || lastAppliedResult.TimeAbsolute < Clock.CurrentTime); + } } } From 57ba7b7cbbbd93870978d047b6edb957cd49d8cf Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 13 Apr 2021 15:55:23 +0900 Subject: [PATCH 09/11] Partially revert the changes of `CurrentFrame` and `NextFrame` for compatibility Making those always non-null is postponed as when a replay's frame contains keypress the behavior is changed. Previously, the key is pressed at the time of the first frame. But using non-null frames means the key is pressed at negative infinity. However, I think the new way of always using non-null frames makes the client code so I plan to bundle the change to more breaking changes. --- .../NonVisual/FramedReplayInputHandlerTest.cs | 22 +++++++++---------- .../Replays/FramedReplayInputHandler.cs | 9 ++++---- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs index 954871595e..a42b7d54ee 100644 --- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs +++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs @@ -84,11 +84,11 @@ namespace osu.Game.Tests.NonVisual // exited important section setTime(8200, 8000); confirmCurrentFrame(7); - confirmNextFrame(7); + confirmNextFrame(null); setTime(8200, 8200); confirmCurrentFrame(7); - confirmNextFrame(7); + confirmNextFrame(null); } [Test] @@ -97,11 +97,11 @@ namespace osu.Game.Tests.NonVisual setReplayFrames(); setTime(-1000, -1000); - confirmCurrentFrame(0); + confirmCurrentFrame(null); confirmNextFrame(0); setTime(-500, -500); - confirmCurrentFrame(0); + confirmCurrentFrame(null); confirmNextFrame(0); setTime(0, 0); @@ -145,7 +145,7 @@ namespace osu.Game.Tests.NonVisual confirmNextFrame(1); setTime(-500, -500); - confirmCurrentFrame(0); + confirmCurrentFrame(null); confirmNextFrame(0); } @@ -231,7 +231,7 @@ namespace osu.Game.Tests.NonVisual Assert.IsFalse(handler.WaitingForFrame, "Should not be waiting yet"); setTime(1000, 1000); confirmCurrentFrame(1); - confirmNextFrame(1); + confirmNextFrame(null); Assert.IsTrue(handler.WaitingForFrame, "Should be waiting"); // cannot seek beyond the last frame @@ -243,7 +243,7 @@ namespace osu.Game.Tests.NonVisual // can seek to the point before the first frame, however setTime(-100, -100); - confirmCurrentFrame(0); + confirmCurrentFrame(null); confirmNextFrame(0); fastForwardToPoint(1000); @@ -311,14 +311,14 @@ namespace osu.Game.Tests.NonVisual Assert.AreEqual(expect, handler.SetFrameFromTime(set), "Unexpected return value"); } - private void confirmCurrentFrame(int frame) + private void confirmCurrentFrame(int? frame) { - Assert.AreEqual(replay.Frames[frame].Time, handler.CurrentFrame.Time, "Unexpected current frame"); + Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.CurrentFrame?.Time, "Unexpected current frame"); } - private void confirmNextFrame(int frame) + private void confirmNextFrame(int? frame) { - Assert.AreEqual(replay.Frames[frame].Time, handler.NextFrame.Time, "Unexpected next frame"); + Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.NextFrame?.Time, "Unexpected next frame"); } private class TestReplayFrame : ReplayFrame diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index c3cd957f0d..a7f11b1e6f 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -30,6 +30,7 @@ namespace osu.Game.Rulesets.Replays /// The current frame of the replay. /// The current time is always between the start and the end time of the current frame. /// + /// Returns null if the current time is strictly before the first frame. /// The replay is empty. public TFrame CurrentFrame { @@ -38,15 +39,15 @@ namespace osu.Game.Rulesets.Replays if (!HasFrames) throw new InvalidOperationException($"Attempted to get {nameof(CurrentFrame)} of an empty replay"); - return (TFrame)Frames[Math.Max(0, currentFrameIndex)]; + return currentFrameIndex == -1 ? null : (TFrame)Frames[currentFrameIndex]; } } /// /// The next frame of the replay. /// The start time is always greater or equal to the start time of regardless of the seeking direction. - /// If it is before the first frame of the replay or the after the last frame of the replay, and agree. /// + /// Returns null if the current frame is the last frame. /// The replay is empty. public TFrame NextFrame { @@ -55,7 +56,7 @@ namespace osu.Game.Rulesets.Replays if (!HasFrames) throw new InvalidOperationException($"Attempted to get {nameof(NextFrame)} of an empty replay"); - return (TFrame)Frames[Math.Min(currentFrameIndex + 1, Frames.Count - 1)]; + return currentFrameIndex == Frames.Count - 1 ? null : (TFrame)Frames[currentFrameIndex + 1]; } } @@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Replays { get { - if (!HasFrames || !FrameAccuratePlayback) + if (!HasFrames || !FrameAccuratePlayback || CurrentFrame == null) return false; return IsImportant(CurrentFrame) && // a button is in a pressed state From 5eaf3ea5765e21a0137b983fa586651d0228ffc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Apr 2021 14:19:06 +0900 Subject: [PATCH 10/11] Reorganise and reword comments to make time override behaviour a bit clearer --- osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index a7f11b1e6f..279087ead9 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -128,13 +128,13 @@ namespace osu.Game.Rulesets.Replays double frameStart = getFrameTime(currentFrameIndex); double frameEnd = getFrameTime(currentFrameIndex + 1); - // If the proposed time is after the current frame end time, we progress forwards. - // If the proposed time is before the current frame start time, and we are at the frame boundary, we progress backwards. + // If the proposed time is after the current frame end time, we progress forwards to precisely the new frame's time (regardless of incoming time). if (frameEnd <= time) { time = frameEnd; currentFrameIndex++; } + // If the proposed time is before the current frame start time, and we are at the frame boundary, we progress backwards. else if (time < frameStart && CurrentTime == frameStart) currentFrameIndex--; From ba325de5959bb83828fc222b359fcb9f9c846236 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Apr 2021 14:19:59 +0900 Subject: [PATCH 11/11] Merge conditionals for readability --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 1c0d820a3d..5ab09f9516 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.UI protected override List GetPendingInputs() { - if (replayInputHandler != null && !replayInputHandler.IsActive) + if (replayInputHandler?.IsActive == false) return emptyInputList; return base.GetPendingInputs();