diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index 987dca7073..ea8d8898c8 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -73,21 +73,11 @@ namespace osu.Game.Rulesets.UI
setClock();
}
- ///
- /// Whether we are running up-to-date with our parent clock.
- /// If not, we will need to keep processing children until we catch up.
- ///
- private bool requireMoreUpdateLoops;
+ private PlaybackState state;
- ///
- /// Whether we are in a valid state (ie. should we keep processing children frames).
- /// This should be set to false when the replay is, for instance, waiting for future frames to arrive.
- ///
- private bool validState;
+ protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid;
- protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState;
-
- private bool isAttached => ReplayInputHandler != null;
+ private bool hasReplayAttached => ReplayInputHandler != null;
private const double sixty_frame_time = 1000.0 / 60;
@@ -95,28 +85,26 @@ namespace osu.Game.Rulesets.UI
public override bool UpdateSubTree()
{
- requireMoreUpdateLoops = true;
-
- validState = !frameStableClock.IsPaused.Value;
-
- int loops = 0;
+ state = frameStableClock.IsPaused.Value ? PlaybackState.NotValid : PlaybackState.Valid;
if (frameStableClock.WaitingOnFrames.Value)
{
// for now, force one update loop to check if frames have arrived
// this may have to change in the future where we want stable user pausing during replay playback.
- validState = true;
+ state = PlaybackState.Valid;
}
- while (validState && requireMoreUpdateLoops && loops++ < MaxCatchUpFrames)
+ int loops = MaxCatchUpFrames;
+
+ while (state != PlaybackState.NotValid && loops-- > 0)
{
updateClock();
- if (validState)
- {
- base.UpdateSubTree();
- UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
- }
+ if (state == PlaybackState.NotValid)
+ break;
+
+ base.UpdateSubTree();
+ UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
}
return true;
@@ -127,94 +115,103 @@ namespace osu.Game.Rulesets.UI
if (parentGameplayClock == null)
setClock(); // LoadComplete may not be run yet, but we still want the clock.
- validState = true;
- requireMoreUpdateLoops = false;
+ // each update start with considering things in valid state.
+ state = PlaybackState.Valid;
- var newProposedTime = parentGameplayClock.CurrentTime;
+ // our goal is to catch up to the time provided by the parent clock.
+ var proposedTime = parentGameplayClock.CurrentTime;
- try
+ if (FrameStablePlayback)
+ // if we require frame stability, the proposed time will be adjusted to move at most one known
+ // frame interval in the current direction.
+ applyFrameStability(ref proposedTime);
+
+ if (hasReplayAttached)
+ state = updateReplay(ref proposedTime);
+
+ if (proposedTime != manualClock.CurrentTime)
+ direction = proposedTime >= manualClock.CurrentTime ? 1 : -1;
+
+ manualClock.CurrentTime = proposedTime;
+ manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction;
+ manualClock.IsRunning = parentGameplayClock.IsRunning;
+
+ double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime);
+
+ // determine whether catch-up is required.
+ if (state != PlaybackState.NotValid)
+ state = timeBehind > 0 ? PlaybackState.RequiresCatchUp : PlaybackState.Valid;
+
+ frameStableClock.IsCatchingUp.Value = timeBehind > 200;
+ frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid;
+
+ // The manual clock time has changed in the above code. The framed clock now needs to be updated
+ // to ensure that the its time is valid for our children before input is processed
+ framedClock.ProcessFrame();
+ }
+
+ ///
+ /// Attempt to advance replay playback for a given time.
+ ///
+ /// The time which is to be displayed.
+ private PlaybackState updateReplay(ref double proposedTime)
+ {
+ double? newTime;
+
+ if (FrameStablePlayback)
{
- if (FrameStablePlayback)
+ // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy.
+ newTime = ReplayInputHandler.SetFrameFromTime(proposedTime);
+ }
+ else
+ {
+ // when stability is disabled, we don't really care about accuracy.
+ // looping over the replay will allow it to catch up and feed out the required values
+ // for the current time.
+ while ((newTime = ReplayInputHandler.SetFrameFromTime(proposedTime)) != proposedTime)
{
- if (firstConsumption)
+ if (newTime == null)
{
- // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
- // Instead we perform an initial seek to the proposed time.
-
- // process frame (in addition to finally clause) to clear out ElapsedTime
- manualClock.CurrentTime = newProposedTime;
- framedClock.ProcessFrame();
-
- firstConsumption = false;
+ // special case for when the replay actually can't arrive at the required time.
+ // protects from potential endless loop.
+ break;
}
- else if (manualClock.CurrentTime < gameplayStartTime)
- manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime);
- else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
- {
- newProposedTime = newProposedTime > manualClock.CurrentTime
- ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
- : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
- }
- }
-
- if (isAttached)
- {
- double? newTime;
-
- if (FrameStablePlayback)
- {
- // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy.
- if ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) == null)
- {
- // setting invalid state here ensures that gameplay will not continue (ie. our child
- // hierarchy won't be updated).
- validState = false;
-
- // potentially loop to catch-up playback.
- requireMoreUpdateLoops = true;
-
- return;
- }
- }
- else
- {
- // when stability is disabled, we don't really care about accuracy.
- // looping over the replay will allow it to catch up and feed out the required values
- // for the current time.
- while ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) != newProposedTime)
- {
- if (newTime == null)
- {
- // special case for when the replay actually can't arrive at the required time.
- // protects from potential endless loop.
- validState = false;
- return;
- }
- }
- }
-
- newProposedTime = newTime.Value;
}
}
- finally
+
+ if (newTime == null)
+ return PlaybackState.NotValid;
+
+ proposedTime = newTime.Value;
+ return PlaybackState.Valid;
+ }
+
+ ///
+ /// Apply frame stability modifier to a time.
+ ///
+ /// The time which is to be displayed.
+ private void applyFrameStability(ref double proposedTime)
+ {
+ if (firstConsumption)
{
- if (newProposedTime != manualClock.CurrentTime)
- direction = newProposedTime >= manualClock.CurrentTime ? 1 : -1;
+ // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
+ // Instead we perform an initial seek to the proposed time.
- manualClock.CurrentTime = newProposedTime;
- manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction;
- manualClock.IsRunning = parentGameplayClock.IsRunning;
-
- double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime);
-
- requireMoreUpdateLoops |= timeBehind != 0;
-
- frameStableClock.IsCatchingUp.Value = timeBehind > 200;
- frameStableClock.WaitingOnFrames.Value = !validState;
-
- // The manual clock time has changed in the above code. The framed clock now needs to be updated
- // to ensure that the its time is valid for our children before input is processed
+ // process frame (in addition to finally clause) to clear out ElapsedTime
+ manualClock.CurrentTime = proposedTime;
framedClock.ProcessFrame();
+
+ firstConsumption = false;
+ return;
+ }
+
+ if (manualClock.CurrentTime < gameplayStartTime)
+ manualClock.CurrentTime = proposedTime = Math.Min(gameplayStartTime, proposedTime);
+ else if (Math.Abs(manualClock.CurrentTime - proposedTime) > sixty_frame_time * 1.2f)
+ {
+ proposedTime = proposedTime > manualClock.CurrentTime
+ ? Math.Min(proposedTime, manualClock.CurrentTime + sixty_frame_time)
+ : Math.Max(proposedTime, manualClock.CurrentTime - sixty_frame_time);
}
}
@@ -233,6 +230,26 @@ namespace osu.Game.Rulesets.UI
public ReplayInputHandler ReplayInputHandler { get; set; }
+ private enum PlaybackState
+ {
+ ///
+ /// Playback is not possible. Child hierarchy should not be processed.
+ ///
+ NotValid,
+
+ ///
+ /// Whether we are running up-to-date with our parent clock.
+ /// If not, we will need to keep processing children until we catch up.
+ ///
+ RequiresCatchUp,
+
+ ///
+ /// Whether we are in a valid state (ie. should we keep processing children frames).
+ /// This should be set to false when the replay is, for instance, waiting for future frames to arrive.
+ ///
+ Valid
+ }
+
private class FrameStabilityClock : GameplayClock, IFrameStableClock
{
public GameplayClock ParentGameplayClock;
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index c3560dff38..25ebd55f81 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -43,8 +43,9 @@ using osuTK.Input;
namespace osu.Game.Screens.Edit
{
[Cached(typeof(IBeatSnapProvider))]
+ [Cached(typeof(ISamplePlaybackDisabler))]
[Cached]
- public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider
+ public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler
{
public override float BackgroundParallaxAmount => 0.1f;
@@ -64,6 +65,10 @@ namespace osu.Game.Screens.Edit
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
+ public IBindable SamplePlaybackDisabled => samplePlaybackDisabled;
+
+ private readonly Bindable samplePlaybackDisabled = new Bindable();
+
private bool exitConfirmed;
private string lastSavedHash;
@@ -109,9 +114,10 @@ namespace osu.Game.Screens.Edit
UpdateClockSource();
dependencies.CacheAs(clock);
- dependencies.CacheAs(clock);
AddInternal(clock);
+ clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState());
+
// todo: remove caching of this and consume via editorBeatmap?
dependencies.Cache(beatDivisor);
@@ -557,40 +563,52 @@ namespace osu.Game.Screens.Edit
.ScaleTo(0.98f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint);
- if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null)
+ try
{
- screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0);
+ if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null)
+ {
+ screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0);
- currentScreen
- .ScaleTo(1, 200, Easing.OutQuint)
- .FadeIn(200, Easing.OutQuint);
- return;
+ currentScreen
+ .ScaleTo(1, 200, Easing.OutQuint)
+ .FadeIn(200, Easing.OutQuint);
+ return;
+ }
+
+ switch (e.NewValue)
+ {
+ case EditorScreenMode.SongSetup:
+ currentScreen = new SetupScreen();
+ break;
+
+ case EditorScreenMode.Compose:
+ currentScreen = new ComposeScreen();
+ break;
+
+ case EditorScreenMode.Design:
+ currentScreen = new DesignScreen();
+ break;
+
+ case EditorScreenMode.Timing:
+ currentScreen = new TimingScreen();
+ break;
+ }
+
+ LoadComponentAsync(currentScreen, newScreen =>
+ {
+ if (newScreen == currentScreen)
+ screenContainer.Add(newScreen);
+ });
}
-
- switch (e.NewValue)
+ finally
{
- case EditorScreenMode.SongSetup:
- currentScreen = new SetupScreen();
- break;
-
- case EditorScreenMode.Compose:
- currentScreen = new ComposeScreen();
- break;
-
- case EditorScreenMode.Design:
- currentScreen = new DesignScreen();
- break;
-
- case EditorScreenMode.Timing:
- currentScreen = new TimingScreen();
- break;
+ updateSampleDisabledState();
}
+ }
- LoadComponentAsync(currentScreen, newScreen =>
- {
- if (newScreen == currentScreen)
- screenContainer.Add(newScreen);
- });
+ private void updateSampleDisabledState()
+ {
+ samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value || !(currentScreen is ComposeScreen);
}
private void seek(UIEvent e, int direction)
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 64ed34f5ec..949636f695 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -11,14 +11,13 @@ using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit
{
///
/// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
///
- public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler
+ public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{
public IBindable