<commit_before>﻿// Copyright (c) ppy Pty Ltd <<EMAIL>>. 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
{
    /// <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
    {
        private readonly Replay replay;

        protected List<ReplayFrame> 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];

                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;

        /// <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 = false;

        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;

        /// <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)
        {
            updateDirection(time);

            Debug.Assert(currentDirection != 0);

            TFrame next = NextFrame;

            // check if the next frame is valid for the current playback direction.
            // validity is if the next frame is equal or "earlier" than the current point in time (so we can change to it)
            int compare = time.CompareTo(next?.Time);

            if (next != null && (compare == 0 || compare == currentDirection))
            {
                currentFrameIndex = clampedNextFrameIndex;
                return CurrentTime = CurrentFrame.Time;
            }

            // at this point, the frame can't be advanced (in the replay).
            // even so, we may be able to move the clock forward due to being at the end of the replay or
            // moving towards the next valid frame.

            // the exception is if currently in an important section, which is respected above all.
            if (inImportantSection)
                return null;

            // in the case we have no next frames and haven't received the full replay, block.
            if (next == null && !replay.HasReceivedAllFrames) return null;

            return CurrentTime = time;
        }

        private void updateDirection(double time)
        {
            if (!CurrentTime.HasValue)
            {
                currentDirection = 1;
            }
            else
            {
                currentDirection = time.CompareTo(CurrentTime);
                if (currentDirection == 0) currentDirection = 1;
            }
        }
    }
}
<commit_msg>Fix FramedReplayInputHandler incorrectly blocking in streaming mode when time requested is before the first frame<commit_after>﻿// Copyright (c) ppy Pty Ltd <<EMAIL>>. 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
{
    /// <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
    {
        private readonly Replay replay;

        protected List<ReplayFrame> 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;

        /// <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 = false;

        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;

        /// <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)
        {
            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;
            }
        }
    }
}

{'fraction_non_alphanumeric': 0.07401272774823468, 'fraction_numerical': 0.004010112457501526, 'mean_word_length': 1.708215297450425, 'pattern_counts': {'":': 0, '<': 34, '<?xml version=': 0, '>': 46, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6435240', 'n_tokens_mistral': 2901, 'n_tokens_neox': 2662, 'n_words': 1009}