// 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.Linq; 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 { /// /// Whether we have at least one replay frame. /// public bool HasFrames => Frames.Count != 0; /// /// Whether we are waiting for new frames to be received. /// public bool WaitingForFrame => !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. /// /// Returns null if the current time is strictly before the first frame. public TFrame? CurrentFrame => currentFrameIndex == -1 ? null : (TFrame)Frames[currentFrameIndex]; /// /// The next frame of the replay. /// The start time of is always greater or equal to the start time of regardless of the seeking direction. /// /// Returns null if the current frame is the last frame. public TFrame? NextFrame => currentFrameIndex == Frames.Count - 1 ? null : (TFrame)Frames[currentFrameIndex + 1]; /// /// The frame for the start value of the interpolation of the replay movement. /// /// The replay is empty. public TFrame StartFrame { get { if (!HasFrames) throw new InvalidOperationException($"Attempted to get {nameof(StartFrame)} of an empty replay"); return (TFrame)Frames[Math.Max(0, currentFrameIndex)]; } } /// /// The frame for the end value of the interpolation of the replay movement. /// /// The replay is empty. public TFrame EndFrame { get { if (!HasFrames) throw new InvalidOperationException($"Attempted to get {nameof(EndFrame)} of an empty replay"); return (TFrame)Frames[Math.Min(currentFrameIndex + 1, Frames.Count - 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; // This input handler should be enabled only if there is at least one replay frame. public override bool IsActive => HasFrames; 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) { // 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 = replay.Frames.OrderBy(f => f.Time).ToList(); this.replay = replay; currentFrameIndex = -1; CurrentTime = double.NegativeInfinity; } private bool inImportantSection { get { if (!HasFrames || !FrameAccuratePlayback || currentFrameIndex == -1) return false; return IsImportant(StartFrame) && // a button is in a pressed state Math.Abs(CurrentTime - EndFrame.Time) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span } } protected virtual bool IsImportant(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 (!HasFrames) { // In the case all frames are received, allow time to progress regardless. if (replay.HasReceivedAllFrames) return CurrentTime = time; return null; } double frameStart = getFrameTime(currentFrameIndex); double frameEnd = getFrameTime(currentFrameIndex + 1); // 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--; frameStart = getFrameTime(currentFrameIndex); frameEnd = getFrameTime(currentFrameIndex + 1); // Pause until more frames are arrived. if (WaitingForFrame && frameStart < time) { CurrentTime = frameStart; return null; } CurrentTime = Math.Clamp(time, frameStart, frameEnd); // In an important section, a mid-frame time cannot be used and a null is returned instead. return inImportantSection && frameStart < time && time < frameEnd ? null : (double?)CurrentTime; } private double getFrameTime(int index) { if (index < 0) return double.NegativeInfinity; if (index >= Frames.Count) return double.PositiveInfinity; return Frames[index].Time; } public sealed override void CollectPendingInputs(List inputs) { base.CollectPendingInputs(inputs); CollectReplayInputs(inputs); if (CurrentFrame?.Header != null) inputs.Add(new ReplayStatisticsFrameInput { Frame = CurrentFrame }); } protected virtual void CollectReplayInputs(List inputs) { } } }