diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index c4563d5844..e90ccbb805 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -104,7 +104,7 @@ namespace osu.Game.Beatmaps string cacheFilePath = storage.GetFullPath(cache_database_name); string compressedCacheFilePath = $"{cacheFilePath}.bz2"; - cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2"); + cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}"); cacheDownloadRequest.Failed += ex => { diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 2c83161614..0248432917 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -95,6 +95,16 @@ namespace osu.Game.Screens.Play localGameplayClock = new LocalGameplayClock(userOffsetClock); GameplayClock.IsPaused.BindTo(IsPaused); + + IsPaused.BindValueChanged(onPauseChanged); + } + + private void onPauseChanged(ValueChangedEvent isPaused) + { + if (isPaused.NewValue) + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop()); + else + this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; @@ -154,13 +164,16 @@ namespace osu.Game.Screens.Play public void Start() { - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the audio clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); - adjustableClock.Start(); - IsPaused.Value = false; + if (!adjustableClock.IsRunning) + { + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the audio clock source potentially taking time to enter a completely stopped state + Seek(GameplayClock.CurrentTime); - this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); + adjustableClock.Start(); + } + + IsPaused.Value = false; } /// @@ -199,8 +212,6 @@ namespace osu.Game.Screens.Play public void Stop() { - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop()); - IsPaused.Value = true; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 42074ac241..729119fa36 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; @@ -71,8 +72,9 @@ namespace osu.Game.Screens.Play } private bool readyForPush => + !playerConsumed // don't push unless the player is completely loaded - player?.LoadState == LoadState.Ready + && player?.LoadState == LoadState.Ready // don't push if the user is hovering one of the panes, unless they are idle. && (IsHovered || idleTracker.IsIdle.Value) // don't push if the user is dragging a slider or otherwise. @@ -84,6 +86,11 @@ namespace osu.Game.Screens.Play private Player player; + /// + /// Whether the curent player instance has been consumed via . + /// + private bool playerConsumed; + private LogoTrackingContainer content; private bool hideOverlays; @@ -179,7 +186,10 @@ namespace osu.Game.Screens.Play contentIn(); MetadataInfo.Delay(750).FadeIn(500); - this.Delay(1800).Schedule(pushWhenLoaded); + + // after an initial delay, start the debounced load check. + // this will continue to execute even after resuming back on restart. + Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, 1800, 0)); showMuteWarningIfNeeded(); } @@ -188,17 +198,18 @@ namespace osu.Game.Screens.Play { base.OnResuming(last); - contentIn(); + // prepare for a retry. + player = null; + playerConsumed = false; + cancelLoad(); - this.Delay(400).Schedule(pushWhenLoaded); + contentIn(); } public override void OnSuspending(IScreen next) { base.OnSuspending(next); - cancelLoad(); - BackgroundBrightnessReduction = false; // we're moving to player, so a period of silence is upcoming. @@ -274,6 +285,14 @@ namespace osu.Game.Screens.Play } } + private Player consumePlayer() + { + Debug.Assert(!playerConsumed); + + playerConsumed = true; + return player; + } + private void prepareNewPlayer() { if (!this.IsCurrentScreen()) @@ -315,64 +334,62 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - try + if (!readyForPush) { - if (!readyForPush) + // as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce + // if we become unready for push during the delay. + cancelLoad(); + return; + } + + // if a push has already been scheduled, no further action is required. + // this value is reset via cancelLoad() to allow a second usage of the same PlayerLoader screen. + if (scheduledPushPlayer != null) + return; + + scheduledPushPlayer = Scheduler.AddDelayed(() => + { + // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). + var consumedPlayer = consumePlayer(); + + contentOut(); + + TransformSequence pushSequence = this.Delay(250); + + // only show if the warning was created (i.e. the beatmap needs it) + // and this is not a restart of the map (the warning expires after first load). + if (epilepsyWarning?.IsAlive == true) { - // as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce - // if we become unready for push during the delay. - cancelLoad(); - return; + const double epilepsy_display_length = 3000; + + pushSequence + .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) + .TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint) + .Delay(epilepsy_display_length) + .Schedule(() => + { + epilepsyWarning.Hide(); + epilepsyWarning.Expire(); + }) + .Delay(EpilepsyWarning.FADE_DURATION); } - if (scheduledPushPlayer != null) - return; - - scheduledPushPlayer = Scheduler.AddDelayed(() => + pushSequence.Schedule(() => { - contentOut(); + if (!this.IsCurrentScreen()) return; - TransformSequence pushSequence = this.Delay(250); + LoadTask = null; - // only show if the warning was created (i.e. the beatmap needs it) - // and this is not a restart of the map (the warning expires after first load). - if (epilepsyWarning?.IsAlive == true) - { - const double epilepsy_display_length = 3000; + // By default, we want to load the player and never be returned to. + // Note that this may change if the player we load requested a re-run. + ValidForResume = false; - pushSequence - .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) - .TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint) - .Delay(epilepsy_display_length) - .Schedule(() => - { - epilepsyWarning.Hide(); - epilepsyWarning.Expire(); - }) - .Delay(EpilepsyWarning.FADE_DURATION); - } - - pushSequence.Schedule(() => - { - if (!this.IsCurrentScreen()) return; - - LoadTask = null; - - // By default, we want to load the player and never be returned to. - // Note that this may change if the player we load requested a re-run. - ValidForResume = false; - - if (player.LoadedBeatmapSuccessfully) - this.Push(player); - else - this.Exit(); - }); - }, 500); - } - finally - { - Schedule(pushWhenLoaded); - } + if (consumedPlayer.LoadedBeatmapSuccessfully) + this.Push(consumedPlayer); + else + this.Exit(); + }); + }, 500); } private void cancelLoad() @@ -390,7 +407,7 @@ namespace osu.Game.Screens.Play if (isDisposing) { // if the player never got pushed, we should explicitly dispose it. - DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose()); + DisposalTask = LoadTask?.ContinueWith(_ => player?.Dispose()); } }