Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix gameplay skipping forward during resume operation #20014

Merged
merged 6 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void TestPauseWithLargeOffset()
Player.OnUpdate += _ =>
{
double currentTime = Player.GameplayClockContainer.CurrentTime;
alwaysGoingForward &= currentTime >= lastTime;
alwaysGoingForward &= currentTime >= lastTime - 500;
lastTime = currentTime;
};
});
Expand All @@ -77,7 +77,7 @@ public void TestPauseWithLargeOffset()

resumeAndConfirm();

AddAssert("time didn't go backwards", () => alwaysGoingForward);
AddAssert("time didn't go too far backwards", () => alwaysGoingForward);

AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0));
}
Expand All @@ -90,6 +90,9 @@ public void TestPauseResume()
AddAssert("player not playing", () => !Player.LocalUserPlaying.Value);

resumeAndConfirm();

AddAssert("Resumed without seeking forward", () => Player.LastResumeTime, () => Is.LessThanOrEqualTo(Player.LastPauseTime));

AddUntilStep("player playing", () => Player.LocalUserPlaying.Value);
}

Expand Down Expand Up @@ -378,14 +381,26 @@ private void confirmPauseOverlayShown(bool isShown) =>
AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown);

private void confirmClockRunning(bool isRunning) =>
AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.IsRunning == isRunning);
AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () =>
{
bool completed = Player.GameplayClockContainer.IsRunning == isRunning;

if (completed)
{
}

return completed;
});

protected override bool AllowFail => true;

protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PausePlayer();

protected class PausePlayer : TestPlayer
{
public double LastPauseTime { get; private set; }
public double LastResumeTime { get; private set; }

public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible;

public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible;
Expand All @@ -399,6 +414,23 @@ public override void OnEntering(ScreenTransitionEvent e)
base.OnEntering(e);
GameplayClockContainer.Stop();
}

private bool? isRunning;

protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();

if (GameplayClockContainer.IsRunning != isRunning)
{
isRunning = GameplayClockContainer.IsRunning;

if (isRunning.Value)
LastResumeTime = GameplayClockContainer.CurrentTime;
else
LastPauseTime = GameplayClockContainer.CurrentTime;
}
}
}
}
}
14 changes: 10 additions & 4 deletions osu.Game/Screens/Play/GameplayClockContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,7 @@ public void Start()

ensureSourceClockSet();

// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
// This accounts for the clock source potentially taking time to enter a completely stopped state
Seek(GameplayClock.CurrentTime);
PrepareStart();

// The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time.
// Because we generally update our own current time quicker than children can query it (via Start/Seek/Update),
Expand All @@ -111,11 +109,19 @@ public void Start()
});
}

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

/// <summary>
/// Seek to a specific time in gameplay.
/// </summary>
/// <param name="time">The destination time to seek to.</param>
public void Seek(double time)
public virtual void Seek(double time)
{
Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}");

Expand Down
32 changes: 32 additions & 0 deletions osu.Game/Screens/Play/MasterGameplayClockContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ public class MasterGameplayClockContainer : GameplayClockContainer, IBeatSyncPro

private readonly List<Bindable<double>> nonGameplayAdjustments = new List<Bindable<double>>();

/// <summary>
/// Stores the time at which the last <see cref="StopGameplayClock"/> call was triggered.
/// This is used to ensure we resume from that precise point in time, ignoring the proceeding frequency ramp.
///
/// Optimally, we'd have gameplay ramp down with the frequency, but I believe this was intentionally disabled
/// to avoid fails occurring after the pause screen has been shown.
///
/// In the future I want to change this.
/// </summary>
private double? actualStopTime;

public override IEnumerable<double> NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value);

/// <summary>
Expand Down Expand Up @@ -86,6 +97,8 @@ private double findEarliestStartTime()

protected override void StopGameplayClock()
{
actualStopTime = GameplayClock.CurrentTime;
smoogipoo marked this conversation as resolved.
Show resolved Hide resolved

if (IsLoaded)
{
// During normal operation, the source is stopped after performing a frequency ramp.
Expand All @@ -108,6 +121,25 @@ protected override void StopGameplayClock()
}
}

public override void Seek(double time)
{
// Safety in case the clock is seeked while stopped.
actualStopTime = null;

base.Seek(time);
}

protected override void PrepareStart()
{
if (actualStopTime != null)
{
Seek(actualStopTime.Value);
actualStopTime = null;
}
else
base.PrepareStart();
}

protected override void StartGameplayClock()
{
addSourceClockAdjustments();
Expand Down