// 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.Diagnostics; using JetBrains.Annotations; using osu.Game.Input.Handlers; using osu.Game.Replays; namespace osu.Game.Rulesets.Replays { /// /// The ReplayHandler will take a replay and handle the propagation of updates to the input stack. /// It handles logic of any frames which *must* be executed. /// public abstract class FramedReplayInputHandler : ReplayInputHandler where TFrame : ReplayFrame { private readonly Replay replay; protected List Frames => replay.Frames; public TFrame CurrentFrame { get { if (!HasFrames || !currentFrameIndex.HasValue) return null; return (TFrame)Frames[currentFrameIndex.Value]; } } public TFrame NextFrame { get { if (!HasFrames) return null; if (!currentFrameIndex.HasValue) return currentDirection > 0 ? (TFrame)Frames[0] : null; int nextFrame = clampedNextFrameIndex; if (nextFrame == currentFrameIndex.Value) return null; return (TFrame)Frames[clampedNextFrameIndex]; } } 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; private bool inImportantSection { get { 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 } } protected virtual bool IsImportant([NotNull] TFrame frame) => false; /// /// Update the current frame based on an incoming time value. /// There are cases where we return a "must-use" time value that is different from the input. /// This is to ensure accurate playback of replay data. /// /// The time which we should use for finding the current frame. /// 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. if (replay.HasReceivedAllFrames) return CurrentTime = time; return null; } TFrame next = NextFrame; // 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) { int compare = time.CompareTo(next.Time); if (compare == 0 || compare == currentDirection) { currentFrameIndex = clampedNextFrameIndex; return CurrentTime = CurrentFrame.Time; } } // 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). // the exception is if currently in an important section, which is respected above all. if (inImportantSection) { Debug.Assert(next != null || !replay.HasReceivedAllFrames); return null; } // if a next frame does exist, allow interpolation. if (next != null) return CurrentTime = time; // 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; } private void updateDirection(double time) { if (!CurrentTime.HasValue) { currentDirection = 1; } else { currentDirection = time.CompareTo(CurrentTime); if (currentDirection == 0) currentDirection = 1; } } } }