2019-03-29 11:38:47 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System ;
using System.Collections.Generic ;
2019-03-31 00:33:56 +08:00
using JetBrains.Annotations ;
2018-04-13 17:19:50 +08:00
using osu.Game.Input.Handlers ;
2018-11-28 16:20:37 +08:00
using osu.Game.Replays ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Replays
{
/// <summary>
/// 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.
/// </summary>
public abstract class FramedReplayInputHandler < TFrame > : ReplayInputHandler
where TFrame : ReplayFrame
{
2021-04-12 14:52:43 +08:00
/// <summary>
/// Whether we have at least one replay frame.
/// </summary>
public bool HasFrames = > Frames . Count ! = 0 ;
2018-04-13 17:19:50 +08:00
2021-04-12 14:52:43 +08:00
/// <summary>
/// Whether we are waiting for new frames to be received.
/// </summary>
2021-04-12 17:50:25 +08:00
public bool WaitingForFrame = > ! replay . HasReceivedAllFrames & & currentFrameIndex = = Frames . Count - 1 ;
2018-04-13 17:19:50 +08:00
2021-04-12 14:52:43 +08:00
/// <summary>
/// The current frame of the replay.
/// The current time is always between the start and the end time of the current frame.
/// </summary>
/// <exception cref="InvalidOperationException">The replay is empty.</exception>
2019-03-29 11:38:40 +08:00
public TFrame CurrentFrame
{
get
{
2021-04-12 10:17:56 +08:00
if ( ! HasFrames )
2021-04-12 14:52:43 +08:00
throw new InvalidOperationException ( $"Attempted to get {nameof(CurrentFrame)} of an empty replay" ) ;
2019-03-29 11:38:40 +08:00
2021-04-12 14:52:43 +08:00
return ( TFrame ) Frames [ Math . Max ( 0 , currentFrameIndex ) ] ;
2019-03-29 11:38:40 +08:00
}
}
2021-04-12 14:52:43 +08:00
/// <summary>
/// The next frame of the replay.
2021-04-12 17:50:54 +08:00
/// The start time is always greater or equal to the start time of <see cref="CurrentFrame"/> regardless of the seeking direction.
2021-04-12 14:52:43 +08:00
/// If it is before the first frame of the replay or the after the last frame of the replay, <see cref="CurrentFrame"/> and <see cref="NextFrame"/> agree.
/// </summary>
/// <exception cref="InvalidOperationException">The replay is empty.</exception>
2019-03-29 11:38:40 +08:00
public TFrame NextFrame
{
get
{
if ( ! HasFrames )
2021-04-12 14:52:43 +08:00
throw new InvalidOperationException ( $"Attempted to get {nameof(NextFrame)} of an empty replay" ) ;
2019-03-29 11:38:40 +08:00
2021-04-12 14:52:43 +08:00
return ( TFrame ) Frames [ Math . Min ( currentFrameIndex + 1 , Frames . Count - 1 ) ] ;
2019-03-29 11:38:40 +08:00
}
}
2018-04-13 17:19:50 +08:00
2021-04-12 14:52:43 +08:00
/// <summary>
/// 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).
/// </summary>
public bool FrameAccuratePlayback ;
2018-04-13 17:19:50 +08:00
2021-04-12 14:52:43 +08:00
// This input handler should be enabled only if there is at least one replay frame.
public override bool IsActive = > HasFrames ;
2018-04-13 17:19:50 +08:00
2021-04-12 14:52:43 +08:00
// Can make it non-null but that is a breaking change.
protected double? CurrentTime { get ; private set ; }
2018-04-13 17:19:50 +08:00
2019-03-28 18:28:13 +08:00
protected virtual double AllowedImportantTimeSpan = > sixty_frame_time * 1.2 ;
2021-04-12 14:52:43 +08:00
protected List < ReplayFrame > Frames = > replay . Frames ;
2019-03-28 18:28:13 +08:00
2021-04-12 14:52:43 +08:00
private readonly Replay replay ;
2018-04-13 17:19:50 +08:00
2021-04-12 14:52:43 +08:00
private int currentFrameIndex ;
2018-04-13 17:19:50 +08:00
2021-04-12 14:52:43 +08:00
private const double sixty_frame_time = 1000.0 / 60 ;
protected FramedReplayInputHandler ( Replay replay )
{
2021-04-12 17:50:54 +08:00
// 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.
2021-04-12 14:52:43 +08:00
replay . Frames . Sort ( ( x , y ) = > x . Time . CompareTo ( y . Time ) ) ;
this . replay = replay ;
currentFrameIndex = - 1 ;
CurrentTime = double . NegativeInfinity ;
}
2018-04-13 17:19:50 +08:00
2019-03-31 00:33:56 +08:00
private bool inImportantSection
{
get
{
if ( ! HasFrames | | ! FrameAccuratePlayback )
return false ;
2021-04-12 14:52:43 +08:00
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
2019-03-31 00:33:56 +08:00
}
}
2018-04-13 17:19:50 +08:00
2019-03-31 00:33:56 +08:00
protected virtual bool IsImportant ( [ NotNull ] TFrame frame ) = > false ;
2018-04-13 17:19:50 +08:00
/// <summary>
/// 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.
/// </summary>
/// <param name="time">The time which we should use for finding the current frame.</param>
/// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns>
public override double? SetFrameFromTime ( double time )
{
2020-10-30 15:09:01 +08:00
if ( ! HasFrames )
{
2021-04-12 14:52:43 +08:00
// In the case all frames are received, allow time to progress regardless.
2020-10-30 15:09:01 +08:00
if ( replay . HasReceivedAllFrames )
return CurrentTime = time ;
return null ;
}
2020-10-28 14:54:58 +08:00
2021-04-12 14:52:43 +08:00
double frameStart = getFrameTime ( currentFrameIndex ) ;
double frameEnd = getFrameTime ( currentFrameIndex + 1 ) ;
2020-10-28 14:54:58 +08:00
2021-04-12 14:52:43 +08:00
// 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 )
2020-10-27 17:28:49 +08:00
{
2021-04-12 14:52:43 +08:00
time = frameEnd ;
currentFrameIndex + + ;
2020-10-27 17:28:49 +08:00
}
2021-04-12 14:52:43 +08:00
else if ( time < frameStart & & CurrentTime = = frameStart )
currentFrameIndex - - ;
2018-04-13 17:19:50 +08:00
2021-04-12 14:52:43 +08:00
frameStart = getFrameTime ( currentFrameIndex ) ;
frameEnd = getFrameTime ( currentFrameIndex + 1 ) ;
2020-10-28 14:54:58 +08:00
2021-04-12 14:52:43 +08:00
// Pause until more frames are arrived.
2021-04-12 17:50:25 +08:00
if ( WaitingForFrame & & frameStart < time )
2020-10-30 15:09:01 +08:00
{
2021-04-12 14:52:43 +08:00
CurrentTime = frameStart ;
2020-10-28 14:54:58 +08:00
return null ;
2020-10-30 15:09:01 +08:00
}
2021-04-12 14:52:43 +08:00
CurrentTime = Math . Clamp ( time , frameStart , frameEnd ) ;
2020-10-30 15:09:01 +08:00
2021-04-12 14:52:43 +08:00
// 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 ;
2018-04-13 17:19:50 +08:00
}
2020-10-28 13:35:42 +08:00
2021-04-12 14:52:43 +08:00
private double getFrameTime ( int index )
2020-10-28 13:35:42 +08:00
{
2021-04-12 14:52:43 +08:00
if ( index < 0 )
return double . NegativeInfinity ;
if ( index > = Frames . Count )
return double . PositiveInfinity ;
return Frames [ index ] . Time ;
2020-10-28 13:35:42 +08:00
}
2018-04-13 17:19:50 +08:00
}
}