// 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 JetBrains.Annotations; using osu.Framework.Input.StateChanges; 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 (TFrame)Frames[0]; if (currentDirection > 0) return currentFrameIndex == Frames.Count - 1 ? null : (TFrame)Frames[currentFrameIndex.Value + 1]; else return currentFrameIndex == 0 ? null : (TFrame)Frames[nextFrameIndex]; } } private int? currentFrameIndex; private int nextFrameIndex => currentFrameIndex.HasValue ? Math.Clamp(currentFrameIndex.Value + (currentDirection > 0 ? 1 : -1), 0, Frames.Count - 1) : 0; protected FramedReplayInputHandler(Replay replay) { this.replay = replay; } private bool advanceFrame() { int newFrame = nextFrameIndex; // ensure we aren't at an extent. if (newFrame == currentFrameIndex) return false; currentFrameIndex = newFrame; return true; } public override List GetPendingInputs() => new List(); 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; /// /// 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 = false; protected 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) { if (!CurrentTime.HasValue) { currentDirection = 1; } else { currentDirection = time.CompareTo(CurrentTime); if (currentDirection == 0) currentDirection = 1; } if (HasFrames) { // check if the next frame is valid for the current playback direction. // validity is if the next frame is equal or "earlier" var compare = time.CompareTo(NextFrame?.Time); if (compare == 0 || compare == currentDirection) { if (advanceFrame()) return CurrentTime = CurrentFrame.Time; } else { // if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. if (inImportantSection) return null; } } return CurrentTime = time; } } }