2019-03-06 19:30:14 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2021-04-15 18:07:25 +08:00
using System ;
2019-03-06 19:30:14 +08:00
using osu.Framework.Allocation ;
2022-09-07 16:38:00 +08:00
using osu.Framework.Audio ;
2019-03-06 19:30:14 +08:00
using osu.Framework.Bindables ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2022-06-28 14:23:29 +08:00
using osu.Framework.Logging ;
2019-03-06 19:30:14 +08:00
using osu.Framework.Timing ;
2022-08-18 13:52:47 +08:00
using osu.Game.Beatmaps ;
2019-03-06 19:30:14 +08:00
namespace osu.Game.Screens.Play
{
2021-04-16 19:33:29 +08:00
/// <summary>
2022-08-15 18:59:08 +08:00
/// Encapsulates gameplay timing logic and provides a <see cref="IGameplayClock"/> via DI for gameplay components to use.
2021-04-16 19:33:29 +08:00
/// </summary>
2022-08-18 13:52:47 +08:00
[Cached(typeof(IGameplayClock))]
2022-08-15 16:36:18 +08:00
public partial class GameplayClockContainer : Container , IAdjustableClock , IGameplayClock
2019-03-06 19:30:14 +08:00
{
2022-08-15 16:36:18 +08:00
public IBindable < bool > IsPaused = > isPaused ;
2023-07-06 13:02:20 +08:00
public bool IsRewinding = > GameplayClock . IsRewinding ;
2021-04-19 18:55:59 +08:00
/// <summary>
2022-08-15 18:59:08 +08:00
/// The source clock. Should generally not be used for any timekeeping purposes.
2021-04-19 18:55:59 +08:00
/// </summary>
2022-08-15 15:56:16 +08:00
public IClock SourceClock { get ; private set ; }
2021-04-19 18:55:59 +08:00
2021-09-20 22:28:58 +08:00
/// <summary>
/// Invoked when a seek has been performed via <see cref="Seek"/>
/// </summary>
2022-08-15 16:06:24 +08:00
public event Action ? OnSeek ;
2021-09-20 22:28:58 +08:00
2022-03-17 19:54:42 +08:00
/// <summary>
2022-03-17 22:39:45 +08:00
/// The time from which the clock should start. Will be seeked to on calling <see cref="Reset"/>.
2022-08-22 13:11:06 +08:00
/// Can be adjusted by calling <see cref="Reset"/> with a time value.
2022-03-17 19:54:42 +08:00
/// </summary>
/// <remarks>
2022-08-18 17:10:20 +08:00
/// By default, a value of zero will be used.
/// Importantly, the value will be inferred from the current beatmap in <see cref="MasterGameplayClockContainer"/> by default.
2022-03-17 19:54:42 +08:00
/// </remarks>
2022-08-23 17:32:56 +08:00
public double StartTime { get ; protected set ; }
2022-07-29 21:12:52 +08:00
2022-09-08 16:14:06 +08:00
public IAdjustableAudioComponent AdjustmentsFromMods { get ; } = new AudioAdjustments ( ) ;
2022-08-15 16:36:18 +08:00
2022-08-15 18:59:08 +08:00
private readonly BindableBool isPaused = new BindableBool ( true ) ;
/// <summary>
/// The adjustable source clock used for gameplay. Should be used for seeks and clock control.
2022-08-26 15:46:22 +08:00
/// This is the final source exposed to gameplay components <see cref="IGameplayClock"/> via delegation in this class.
2022-08-15 18:59:08 +08:00
/// </summary>
2022-08-18 13:52:47 +08:00
protected readonly FramedBeatmapClock GameplayClock ;
protected override Container < Drawable > Content { get ; } = new Container { RelativeSizeAxes = Axes . Both } ;
2022-08-15 16:06:24 +08:00
2021-04-16 19:33:29 +08:00
/// <summary>
/// Creates a new <see cref="GameplayClockContainer"/>.
/// </summary>
/// <param name="sourceClock">The source <see cref="IClock"/> used for timing.</param>
2022-08-18 13:52:47 +08:00
/// <param name="applyOffsets">Whether to apply platform, user and beatmap offsets to the mix.</param>
public GameplayClockContainer ( IClock sourceClock , bool applyOffsets = false )
2019-03-06 19:30:14 +08:00
{
2021-04-19 18:55:59 +08:00
SourceClock = sourceClock ;
2021-04-14 16:47:11 +08:00
RelativeSizeAxes = Axes . Both ;
2019-03-06 19:30:14 +08:00
2022-08-18 13:52:47 +08:00
InternalChildren = new Drawable [ ]
{
2022-08-23 17:32:56 +08:00
GameplayClock = new FramedBeatmapClock ( applyOffsets ) { IsCoupled = false } ,
2022-08-18 13:52:47 +08:00
Content
} ;
2021-04-14 16:47:11 +08:00
}
2019-03-06 19:30:14 +08:00
2021-04-16 19:33:29 +08:00
/// <summary>
2022-08-19 00:40:12 +08:00
/// Starts gameplay and marks un-paused state.
2021-04-16 19:33:29 +08:00
/// </summary>
2022-08-22 18:43:18 +08:00
public void Start ( )
2021-04-14 16:47:11 +08:00
{
2022-08-22 18:43:18 +08:00
if ( ! isPaused . Value )
return ;
2021-04-20 12:56:13 +08:00
2022-08-19 00:40:12 +08:00
isPaused . Value = false ;
2022-08-22 18:43:18 +08:00
ensureSourceClockSet ( ) ;
2022-08-29 18:51:16 +08:00
PrepareStart ( ) ;
2022-08-22 18:43:18 +08:00
2022-08-26 14:20:09 +08:00
// The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time.
2022-08-22 18:43:18 +08:00
// Because we generally update our own current time quicker than children can query it (via Start/Seek/Update),
// this means that the first frame ever exposed to children may have a non-zero current time.
//
// If the child component is not aware of the parent ElapsedFrameTime (which is the case for FrameStabilityContainer)
// they will take on the new CurrentTime with a zero elapsed time. This can in turn cause components to behave incorrectly
// if they are intending to trigger events at the precise StartTime (ie. DrawableStoryboardSample).
//
// By scheduling the start call, children are guaranteed to receive one frame at the original start time, allowing
// then to progress with a correct locally calculated elapsed time.
SchedulerAfterChildren . Add ( ( ) = >
2021-04-14 16:47:11 +08:00
{
2022-08-22 18:43:18 +08:00
if ( isPaused . Value )
return ;
StartGameplayClock ( ) ;
} ) ;
2021-04-14 16:47:11 +08:00
}
2019-03-25 19:25:47 +08:00
2022-08-29 18:51:16 +08:00
/// <summary>
/// When <see cref="Start"/> is called, this will be run to give an opportunity to prepare the clock at the correct
/// start location.
/// </summary>
protected virtual void PrepareStart ( )
{
}
2021-04-14 16:47:11 +08:00
/// <summary>
/// Seek to a specific time in gameplay.
/// </summary>
/// <param name="time">The destination time to seek to.</param>
2022-08-29 18:51:16 +08:00
public virtual void Seek ( double time )
2021-04-20 16:35:59 +08:00
{
2022-06-28 14:23:29 +08:00
Logger . Log ( $"{nameof(GameplayClockContainer)} seeking to {time}" ) ;
2022-08-18 13:52:47 +08:00
GameplayClock . Seek ( time ) ;
2021-04-20 16:35:59 +08:00
2021-09-20 22:28:58 +08:00
OnSeek ? . Invoke ( ) ;
2021-04-20 16:35:59 +08:00
}
2020-12-10 16:42:47 +08:00
2021-04-16 19:33:29 +08:00
/// <summary>
2022-08-19 00:40:12 +08:00
/// Stops gameplay and marks paused state.
2021-04-16 19:33:29 +08:00
/// </summary>
2022-08-22 18:43:18 +08:00
public void Stop ( )
{
if ( isPaused . Value )
return ;
isPaused . Value = true ;
StopGameplayClock ( ) ;
}
protected virtual void StartGameplayClock ( ) = > GameplayClock . Start ( ) ;
protected virtual void StopGameplayClock ( ) = > GameplayClock . Stop ( ) ;
2020-12-10 16:42:47 +08:00
2021-04-16 19:33:29 +08:00
/// <summary>
2021-04-19 18:53:55 +08:00
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.
2021-04-16 19:33:29 +08:00
/// </summary>
2022-08-22 13:11:06 +08:00
/// <param name="time">The time to seek to on resetting. If <c>null</c>, the existing <see cref="StartTime"/> will be used.</param>
2022-03-18 14:08:25 +08:00
/// <param name="startClock">Whether to start the clock immediately, if not already started.</param>
2022-08-22 13:11:06 +08:00
public void Reset ( double? time = null , bool startClock = false )
2020-12-10 16:42:47 +08:00
{
2022-08-22 18:43:18 +08:00
bool wasPaused = isPaused . Value ;
Stop ( ) ;
2021-04-14 16:47:11 +08:00
2022-04-11 13:10:50 +08:00
ensureSourceClockSet ( ) ;
2022-08-22 13:11:06 +08:00
if ( time ! = null )
StartTime = time . Value ;
2022-08-18 17:10:20 +08:00
Seek ( StartTime ) ;
2022-08-19 00:40:12 +08:00
2023-08-02 17:26:36 +08:00
// This is a workaround for the fact that DecoupleableInterpolatingFramedClock doesn't seek the source
// if the source is not IsRunning. (see https://github.com/ppy/osu-framework/blob/2102638056dfcf85d21b4d85266d53b5dd018767/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs#L209-L210)
// I hope to remove this once we knock some sense into clocks in general.
2023-08-16 15:38:49 +08:00
//
// Without this seek, the multiplayer spectator start sequence breaks:
// - Individual clients' clocks are never updated to their expected time
// - The sync manager thinks they are running behind
// - Gameplay doesn't start when it should (until a timeout occurs because nothing is happening for 10+ seconds)
//
// In addition, we use `CurrentTime` for this seek instead of `StartTime` as the above seek may have applied inherent
2023-08-16 16:07:12 +08:00
// offsets which need to be accounted for (ie. FramedBeatmapClock.TotalAppliedOffset).
2023-08-16 15:38:49 +08:00
//
// See https://github.com/ppy/osu/pull/24451/files/87fee001c786b29db34063ef3350e9a9f024d3ab#diff-28ca02979641e2d98a15fe5d5e806f56acf60ac100258a059fa72503b6cc54e8.
( SourceClock as IAdjustableClock ) ? . Seek ( CurrentTime ) ;
2023-08-02 17:26:36 +08:00
2022-08-22 18:43:18 +08:00
if ( ! wasPaused | | startClock )
2022-08-19 00:40:12 +08:00
Start ( ) ;
2019-03-06 19:30:14 +08:00
}
2021-04-19 18:55:59 +08:00
/// <summary>
/// Changes the source clock.
/// </summary>
/// <param name="sourceClock">The new source.</param>
2022-08-18 13:52:47 +08:00
protected void ChangeSource ( IClock sourceClock ) = > GameplayClock . ChangeSource ( SourceClock = sourceClock ) ;
2021-04-19 18:55:59 +08:00
2021-04-24 03:56:08 +08:00
/// <summary>
2022-08-18 13:52:47 +08:00
/// Ensures that the <see cref="GameplayClock"/> is set to <see cref="SourceClock"/>, if it hasn't been given a source yet.
2021-04-24 03:56:08 +08:00
/// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode,
/// but not the actual source clock.
/// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor,
/// but it is not yet set on the adjustable source there.
/// </summary>
2021-04-24 20:19:39 +08:00
private void ensureSourceClockSet ( )
{
2022-08-18 13:52:47 +08:00
if ( GameplayClock . Source = = null )
2021-04-24 20:19:39 +08:00
ChangeSource ( SourceClock ) ;
}
2021-04-24 03:56:08 +08:00
2021-04-15 18:07:25 +08:00
#region IAdjustableClock
bool IAdjustableClock . Seek ( double position )
{
Seek ( position ) ;
return true ;
}
2021-04-21 16:13:01 +08:00
void IAdjustableClock . Reset ( ) = > Reset ( ) ;
2021-04-15 18:07:25 +08:00
2022-09-05 22:22:38 +08:00
public virtual void ResetSpeedAdjustments ( )
{
}
2021-04-15 18:07:25 +08:00
double IAdjustableClock . Rate
{
2022-08-18 13:52:47 +08:00
get = > GameplayClock . Rate ;
2021-04-15 18:07:25 +08:00
set = > throw new NotSupportedException ( ) ;
}
2022-08-18 13:52:47 +08:00
public double Rate = > GameplayClock . Rate ;
2021-04-15 18:07:25 +08:00
2022-08-18 13:52:47 +08:00
public double CurrentTime = > GameplayClock . CurrentTime ;
2021-04-15 18:07:25 +08:00
2022-08-18 13:52:47 +08:00
public bool IsRunning = > GameplayClock . IsRunning ;
2021-04-15 18:07:25 +08:00
#endregion
2022-08-15 16:06:24 +08:00
public void ProcessFrame ( )
{
// Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times.
}
2022-08-18 13:52:47 +08:00
public double ElapsedFrameTime = > GameplayClock . ElapsedFrameTime ;
2022-08-15 16:06:24 +08:00
2022-08-18 13:52:47 +08:00
public double FramesPerSecond = > GameplayClock . FramesPerSecond ;
2022-08-15 16:06:24 +08:00
2022-08-18 13:52:47 +08:00
public FrameTimeInfo TimeInfo = > GameplayClock . TimeInfo ;
2021-04-14 16:47:11 +08:00
}
2019-03-06 19:30:14 +08:00
}