From 95c1b488a7ebfcf4872b5249f9e266940c7d7fb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 16:55:55 +0900 Subject: [PATCH 01/14] Add non-null assertion to `FrameStabilityContainer` --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index dcd7141419..b4a0d83bf2 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -129,6 +130,8 @@ namespace osu.Game.Rulesets.UI if (parentGameplayClock == null) setClock(); // LoadComplete may not be run yet, but we still want the clock. + Debug.Assert(parentGameplayClock != null); + double proposedTime = parentGameplayClock.CurrentTime; if (FrameStablePlayback) From 224f3eaa8470a0ea66e596122e0fb0fc78d6c788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 16:56:16 +0900 Subject: [PATCH 02/14] Make `GameplayClockContainer` non-`abstract` and use in `MultiSpectatorPlayer` --- .../Spectate/MultiSpectatorPlayer.cs | 43 +++++++------------ .../Screens/Play/GameplayClockContainer.cs | 8 ++-- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index a0558f97a9..68eae76030 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -1,12 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -26,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// /// The score containing the player's replay. /// The clock controlling the gameplay running state. - public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock) + public MultiSpectatorPlayer(Score score, ISpectatorPlayerClock spectatorPlayerClock) : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.spectatorPlayerClock = spectatorPlayerClock; @@ -41,6 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate HUDOverlay.HoldToQuit.Expire(); } + protected override void Update() + { + // The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay. + CatchUpSpectatorPlayerClock catchUpClock = (CatchUpSpectatorPlayerClock)GameplayClockContainer.SourceClock; + + if (catchUpClock.IsRunning) + GameplayClockContainer.Start(); + else + GameplayClockContainer.Stop(); + + base.Update(); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -50,28 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) - => new SpectatorGameplayClockContainer(spectatorPlayerClock); - - private class SpectatorGameplayClockContainer : GameplayClockContainer - { - public SpectatorGameplayClockContainer([NotNull] IClock sourceClock) - : base(sourceClock) - { - } - - protected override void Update() - { - // The SourceClock here is always a CatchUpSpectatorPlayerClock. - // The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay. - if (SourceClock.IsRunning) - Start(); - else - Stop(); - - base.Update(); - } - - protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); - } + => new GameplayClockContainer(spectatorPlayerClock); } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index b37d15e06c..ffecb1d9a5 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Play /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// - public abstract class GameplayClockContainer : Container, IAdjustableClock + public class GameplayClockContainer : Container, IAdjustableClock { /// /// The final clock which is exposed to gameplay components. @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Play /// /// The source clock. /// - protected IClock SourceClock { get; private set; } + public IClock SourceClock { get; private set; } /// /// Invoked when a seek has been performed via @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play /// Creates a new . /// /// The source used for timing. - protected GameplayClockContainer(IClock sourceClock) + public GameplayClockContainer(IClock sourceClock) { SourceClock = sourceClock; @@ -193,7 +193,7 @@ namespace osu.Game.Screens.Play /// /// The providing the source time. /// The final . - protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source); + protected virtual GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source); #region IAdjustableClock From 6d782181427564409bb9c4b030f1e49d87db4dd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 17:06:24 +0900 Subject: [PATCH 03/14] Update usages of `GameplayClockContainer.GameplayClock` to access properties directly --- .../Mods/TestSceneOsuModDoubleTime.cs | 2 +- .../TestSceneMasterGameplayClockContainer.cs | 8 ++--- .../Gameplay/TestSceneStoryboardSamples.cs | 2 +- .../Visual/Gameplay/TestSceneLeadIn.cs | 4 +-- .../Gameplay/TestSceneOverlayActivation.cs | 2 +- .../Visual/Gameplay/TestScenePause.cs | 4 +-- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 5 +-- .../Visual/Gameplay/TestSceneSongProgress.cs | 2 +- .../Gameplay/TestSceneStoryboardWithOutro.cs | 10 +++--- .../TestSceneMultiSpectatorScreen.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 2 +- .../Spectate/MultiSpectatorScreen.cs | 2 +- .../Screens/Play/GameplayClockContainer.cs | 32 ++++++++++++------- osu.Game/Screens/Play/Player.cs | 8 ++--- 14 files changed, 48 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs index 335ef31019..8df8afe147 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = mod, PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 && - Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value) + Precision.AlmostEquals(Player.GameplayClockContainer.Rate, mod.SpeedChange.Value) }); } } diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 5f403f9487..abd734b96c 100644 --- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Gameplay }); AddStep("start clock", () => gameplayClockContainer.Start()); - AddUntilStep("elapsed greater than zero", () => gameplayClockContainer.GameplayClock.ElapsedFrameTime > 0); + AddUntilStep("elapsed greater than zero", () => gameplayClockContainer.ElapsedFrameTime > 0); } [Test] @@ -60,16 +60,16 @@ namespace osu.Game.Tests.Gameplay }); AddStep("start clock", () => gameplayClockContainer.Start()); - AddUntilStep("current time greater 2000", () => gameplayClockContainer.GameplayClock.CurrentTime > 2000); + AddUntilStep("current time greater 2000", () => gameplayClockContainer.CurrentTime > 2000); double timeAtReset = 0; AddStep("reset clock", () => { - timeAtReset = gameplayClockContainer.GameplayClock.CurrentTime; + timeAtReset = gameplayClockContainer.CurrentTime; gameplayClockContainer.Reset(); }); - AddAssert("current time < time at reset", () => gameplayClockContainer.GameplayClock.CurrentTime < timeAtReset); + AddAssert("current time < time at reset", () => gameplayClockContainer.CurrentTime < timeAtReset); } [Test] diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index f05244ab88..3ccf6c5d33 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Gameplay beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { - Clock = gameplayContainer.GameplayClock + Clock = gameplayContainer }); }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 7f4276f819..0d80d29cab 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Gameplay public double FirstHitObjectTime => DrawableRuleset.Objects.First().StartTime; - public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime; + public double GameplayClockTime => GameplayClockContainer.CurrentTime; protected override void UpdateAfterChildren() { @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Gameplay if (!FirstFrameClockTime.HasValue) { - FirstFrameClockTime = GameplayClockContainer.GameplayClock.CurrentTime; + FirstFrameClockTime = GameplayClockContainer.CurrentTime; AddInternal(new OsuSpriteText { Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} " diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 3e637f1870..789e7e770f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay base.SetUpSteps(); AddUntilStep("gameplay has started", - () => Player.GameplayClockContainer.GameplayClock.CurrentTime > Player.DrawableRuleset.GameplayStartTime); + () => Player.GameplayClockContainer.CurrentTime > Player.DrawableRuleset.GameplayStartTime); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 71cc1f7b23..cad8c62233 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -313,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("pause again", () => { Player.Pause(); - return !Player.GameplayClockContainer.GameplayClock.IsRunning; + return !Player.GameplayClockContainer.IsRunning; }); AddAssert("loop is playing", () => getLoop().IsPlaying); @@ -378,7 +378,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown); private void confirmClockRunning(bool isRunning) => - AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning); + AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.IsRunning == isRunning); protected override bool AllowFail => true; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 5c73db15df..b6b3650c83 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osuTK; @@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay private double increment; private GameplayClockContainer gameplayClockContainer; - private GameplayClock gameplayClock; + private IFrameBasedClock gameplayClock; private const double skip_time = 6000; @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; gameplayClockContainer.Start(); - gameplayClock = gameplayClockContainer.GameplayClock; + gameplayClock = gameplayClockContainer; }); [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 9eb71b9cf7..146cbfb052 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)); - Dependencies.CacheAs(gameplayClockContainer.GameplayClock); + Dependencies.CacheAs(gameplayClockContainer); } [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index e2b2ad85a3..e2c825df0b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestStoryboardNoSkipOutro() { CreateTest(); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for score shown", () => Player.IsScoreShown); } @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); } @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("set ShowResults = false", () => showResults = false); }); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddWaitStep("wait", 10); AddAssert("no score shown", () => !Player.IsScoreShown); } @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestStoryboardEndsBeforeCompletion() { CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100)); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("wait for score shown", () => Player.IsScoreShown); } @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden); AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible); - AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); + AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 706d493fd6..a2e3ab7318 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -451,7 +451,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void checkPaused(int userId, bool state) - => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().GameplayClock.IsRunning != state); + => AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType().First().IsRunning != state); private void checkPausedInstant(int userId, bool state) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 269867be73..6098a3e794 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -671,7 +671,7 @@ namespace osu.Game.Tests.Visual.Multiplayer for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000) { double time = i; - AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.GameplayClock.CurrentTime > time); + AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.CurrentTime > time); } AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 5cd9e0ddf9..7ed0be50e5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate for (int i = 0; i < Users.Count; i++) { - grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer.GameplayClock)); + grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer)); syncManager.AddPlayerClock(instances[i].GameplayClock); } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ffecb1d9a5..2cc1fe8eaf 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -16,13 +15,8 @@ namespace osu.Game.Screens.Play /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// - public class GameplayClockContainer : Container, IAdjustableClock + public class GameplayClockContainer : Container, IAdjustableClock, IFrameBasedClock { - /// - /// The final clock which is exposed to gameplay components. - /// - public GameplayClock GameplayClock { get; private set; } - /// /// Whether gameplay is paused. /// @@ -41,7 +35,7 @@ namespace osu.Game.Screens.Play /// /// Invoked when a seek has been performed via /// - public event Action OnSeek; + public event Action? OnSeek; private double? startTime; @@ -59,11 +53,16 @@ namespace osu.Game.Screens.Play { startTime = value; - if (GameplayClock != null) + if (GameplayClock.IsNotNull()) GameplayClock.StartTime = value; } } + /// + /// The final clock which is exposed to gameplay components. + /// + protected GameplayClock GameplayClock { get; private set; } = null!; + /// /// Creates a new . /// @@ -215,12 +214,23 @@ namespace osu.Game.Screens.Play set => throw new NotSupportedException(); } - double IClock.Rate => GameplayClock.Rate; + public double Rate => GameplayClock.Rate; public double CurrentTime => GameplayClock.CurrentTime; public bool IsRunning => GameplayClock.IsRunning; #endregion + + public void ProcessFrame() + { + // Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times. + } + + public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime; + + public double FramesPerSecond => GameplayClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 08b6da1921..0ef09e4029 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -475,7 +475,7 @@ namespace osu.Game.Screens.Play private void updateSampleDisabledState() { - samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.IsCatchingUp.Value || GameplayClockContainer.GameplayClock.IsPaused.Value; + samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.IsCatchingUp.Value || GameplayClockContainer.IsPaused.Value; } private void updatePauseOnFocusLostState() @@ -877,7 +877,7 @@ namespace osu.Game.Screens.Play private double? lastPauseActionTime; protected bool PauseCooldownActive => - lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; + lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + pause_cooldown; /// /// A set of conditionals which defines whether the current game state and configuration allows for @@ -915,7 +915,7 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Stop(); PauseOverlay.Show(); - lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; + lastPauseActionTime = GameplayClockContainer.CurrentTime; return true; } @@ -1005,7 +1005,7 @@ namespace osu.Game.Screens.Play /// protected virtual void StartGameplay() { - if (GameplayClockContainer.GameplayClock.IsRunning) + if (GameplayClockContainer.IsRunning) throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); GameplayClockContainer.Reset(true); From c8764cb3336ffd3818d5df49f2d1e226770c0fd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 17:11:22 +0900 Subject: [PATCH 04/14] Move all usage of `GameplayClock` to `IGameplayClock` --- .../Default/SpinnerRotationTracker.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- .../TestSceneSkinEditorMultipleSkins.cs | 2 +- .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- .../Rulesets/UI/FrameStabilityContainer.cs | 8 ++--- osu.Game/Screens/Play/ComboEffects.cs | 2 +- osu.Game/Screens/Play/GameplayClock.cs | 18 ++-------- .../Screens/Play/GameplayClockContainer.cs | 2 +- osu.Game/Screens/Play/HUD/SongProgress.cs | 2 +- osu.Game/Screens/Play/HUD/SongProgressInfo.cs | 4 +-- osu.Game/Screens/Play/IGameplayClock.cs | 34 +++++++++++++++++++ osu.Game/Screens/Play/SkipOverlay.cs | 2 +- .../Drawables/DrawableStoryboard.cs | 2 +- 14 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 osu.Game/Screens/Play/IGameplayClock.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index f9ed8b8721..554ea3ac90 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private bool rotationTransferred; [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } + private IGameplayClock gameplayClock { get; set; } protected override void Update() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index d4f3d0f390..d6c49b026e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay (typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get()), (typeof(GameplayState), actualComponentsContainer.Dependencies.Get()), - (typeof(GameplayClock), actualComponentsContainer.Dependencies.Get()) + (typeof(IGameplayClock), actualComponentsContainer.Dependencies.Get()) }, }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index fb97f94dbb..574f749e28 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 3eb92b3e97..e694b396ad 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index ee2827122d..6b990ce93c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); private IEnumerable hudOverlays => CreatedDrawables.OfType(); diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index b4a0d83bf2..bfad22b4f0 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.UI public IFrameStableClock FrameStableClock => frameStableClock; - [Cached(typeof(GameplayClock))] + [Cached(typeof(IGameplayClock))] private readonly FrameStabilityClock frameStableClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) @@ -64,12 +64,12 @@ namespace osu.Game.Rulesets.UI private int direction = 1; [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock) + private void load(IGameplayClock clock) { if (clock != null) { parentGameplayClock = frameStableClock.ParentGameplayClock = clock; - frameStableClock.IsPaused.BindTo(clock.IsPaused); + ((IBindable)frameStableClock.IsPaused).BindTo(clock.IsPaused); } } @@ -272,7 +272,7 @@ namespace osu.Game.Rulesets.UI private class FrameStabilityClock : GameplayClock, IFrameStableClock { - public GameplayClock ParentGameplayClock; + public IGameplayClock ParentGameplayClock; public readonly Bindable IsCatchingUp = new Bindable(); diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 77681401bb..442b061af7 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Play private ISamplePlaybackDisabler samplePlaybackDisabler { get; set; } [Resolved] - private GameplayClock gameplayClock { get; set; } + private IGameplayClock gameplayClock { get; set; } private void onComboChange(ValueChangedEvent combo) { diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 6af795cfd8..454229fb31 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -19,15 +19,14 @@ namespace osu.Game.Screens.Play /// , as this should only be done once to ensure accuracy. /// /// - public class GameplayClock : IFrameBasedClock + public class GameplayClock : IGameplayClock { internal readonly IFrameBasedClock UnderlyingClock; public readonly BindableBool IsPaused = new BindableBool(); - /// - /// All adjustments applied to this clock which don't come from gameplay or mods. - /// + IBindable IGameplayClock.IsPaused => IsPaused; + public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); public GameplayClock(IFrameBasedClock underlyingClock) @@ -35,23 +34,12 @@ namespace osu.Game.Screens.Play UnderlyingClock = underlyingClock; } - /// - /// The time from which the clock should start. Will be seeked to on calling . - /// - /// - /// If not set, a value of zero will be used. - /// Importantly, the value will be inferred from the current ruleset in unless specified. - /// public double? StartTime { get; internal set; } public double CurrentTime => UnderlyingClock.CurrentTime; public double Rate => UnderlyingClock.Rate; - /// - /// The rate of gameplay when playback is at 100%. - /// This excludes any seeking / user adjustments. - /// public double TrueGameplayRate { get diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 2cc1fe8eaf..f7f115eddb 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); + dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); GameplayClock.StartTime = StartTime; GameplayClock.IsPaused.BindTo(IsPaused); diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 85bb42193b..f368edbfb9 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } [Resolved] - protected GameplayClock GameplayClock { get; private set; } = null!; + protected IGameplayClock GameplayClock { get; private set; } = null!; [Resolved(canBeNull: true)] private DrawableRuleset? drawableRuleset { get; set; } diff --git a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs index 8f10e84509..96a4c5f2bc 100644 --- a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs +++ b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs @@ -38,10 +38,10 @@ namespace osu.Game.Screens.Play.HUD set => endTime = value; } - private GameplayClock gameplayClock; + private IGameplayClock gameplayClock; [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, GameplayClock clock) + private void load(OsuColour colours, IGameplayClock clock) { if (clock != null) gameplayClock = clock; diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs new file mode 100644 index 0000000000..c3d61be5d5 --- /dev/null +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Timing; + +namespace osu.Game.Screens.Play +{ + public interface IGameplayClock : IFrameBasedClock + { + /// + /// The rate of gameplay when playback is at 100%. + /// This excludes any seeking / user adjustments. + /// + double TrueGameplayRate { get; } + + /// + /// The time from which the clock should start. Will be seeked to on calling . + /// + /// + /// If not set, a value of zero will be used. + /// Importantly, the value will be inferred from the current ruleset in unless specified. + /// + double? StartTime { get; } + + /// + /// All adjustments applied to this clock which don't come from gameplay or mods. + /// + IEnumerable> NonGameplayAdjustments { get; } + + IBindable IsPaused { get; } + } +} diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3e2cf9a756..687705ff1b 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private bool isClickable; [Resolved] - private GameplayClock gameplayClock { get; set; } + private IGameplayClock gameplayClock { get; set; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 8343f14050..6295604438 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -85,7 +85,7 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm) + private void load(IGameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm) { if (clock != null) Clock = clock; From f81c7644b40b9348bcc92bad75ae1559e0e975f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 17:36:18 +0900 Subject: [PATCH 05/14] Make `GameplayClockContainer` also an `IGameplayClock` and expose to remaining tests --- .../Gameplay/TestSceneStoryboardSamples.cs | 2 -- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- .../TestSceneSkinEditorMultipleSkins.cs | 2 +- .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- .../Visual/Gameplay/TestSceneSongProgress.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 17 ++++++++++++----- osu.Game/Screens/Play/Player.cs | 2 +- .../Screens/Play/ScreenSuspensionHandler.cs | 2 +- 8 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 3ccf6c5d33..18ae2cb7c8 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -77,7 +77,6 @@ namespace osu.Game.Tests.Gameplay Add(gameplayContainer = new MasterGameplayClockContainer(working, 0) { - IsPaused = { Value = true }, Child = new FrameStabilityContainer { Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) @@ -106,7 +105,6 @@ namespace osu.Game.Tests.Gameplay Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time) { StartTime = start_time, - IsPaused = { Value = true }, Child = new FrameStabilityContainer { Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 574f749e28..3e2698bc05 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] + [Cached(typeof(IGameplayClock))] private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); // best way to check without exposing. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index e694b396ad..e29101ba8d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] + [Cached(typeof(IGameplayClock))] private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 6b990ce93c..00e4171eac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] + [Cached(typeof(IGameplayClock))] private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); private IEnumerable hudOverlays => CreatedDrawables.OfType(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 146cbfb052..3487f4dbff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)); - Dependencies.CacheAs(gameplayClockContainer); + Dependencies.CacheAs(gameplayClockContainer); } [SetUpSteps] diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index f7f115eddb..8400e0a9c2 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -15,12 +16,14 @@ namespace osu.Game.Screens.Play /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// - public class GameplayClockContainer : Container, IAdjustableClock, IFrameBasedClock + public class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { /// /// Whether gameplay is paused. /// - public readonly BindableBool IsPaused = new BindableBool(true); + public IBindable IsPaused => isPaused; + + private readonly BindableBool isPaused = new BindableBool(true); /// /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. @@ -58,6 +61,8 @@ namespace osu.Game.Screens.Play } } + public IEnumerable> NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; + /// /// The final clock which is exposed to gameplay components. /// @@ -84,7 +89,7 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); GameplayClock.StartTime = StartTime; - GameplayClock.IsPaused.BindTo(IsPaused); + GameplayClock.IsPaused.BindTo(isPaused); return dependencies; } @@ -105,7 +110,7 @@ namespace osu.Game.Screens.Play AdjustableSource.Start(); } - IsPaused.Value = false; + isPaused.Value = false; } /// @@ -127,7 +132,7 @@ namespace osu.Game.Screens.Play /// /// Stops gameplay. /// - public void Stop() => IsPaused.Value = true; + public void Stop() => isPaused.Value = true; /// /// Resets this and the source to an initial state ready for gameplay. @@ -232,5 +237,7 @@ namespace osu.Game.Screens.Play public double FramesPerSecond => GameplayClock.FramesPerSecond; public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; + + public double TrueGameplayRate => GameplayClock.TrueGameplayRate; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0ef09e4029..51b2042d1b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -330,7 +330,7 @@ namespace osu.Game.Screens.Play DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); // bind clock into components that require it - DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); + ((IBindable)DrawableRuleset.IsPaused).BindTo(GameplayClockContainer.IsPaused); DrawableRuleset.NewResult += r => { diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 59b92a1b97..cc1254975c 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play public class ScreenSuspensionHandler : Component { private readonly GameplayClockContainer gameplayClockContainer; - private Bindable isPaused; + private IBindable isPaused; private readonly Bindable disableSuspensionBindable = new Bindable(); From c5f8529d20a57f198a4fb6be7c68090cef7c4fe3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 17:57:26 +0900 Subject: [PATCH 06/14] Mark unused methods as `NotImplemented` for safety --- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 8400e0a9c2..bf689dcfe1 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -209,9 +209,7 @@ namespace osu.Game.Screens.Play void IAdjustableClock.Reset() => Reset(); - public void ResetSpeedAdjustments() - { - } + public void ResetSpeedAdjustments() => throw new NotImplementedException(); double IAdjustableClock.Rate { From cc982d374cf569d645051e87249a47a46b45b2f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 18:14:57 +0900 Subject: [PATCH 07/14] Cache self rather than `GameplayClock` --- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index bf689dcfe1..df1eb32f99 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -86,7 +86,9 @@ namespace osu.Game.Screens.Play { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); + GameplayClock = CreateGameplayClock(AdjustableSource); + + dependencies.CacheAs(this); GameplayClock.StartTime = StartTime; GameplayClock.IsPaused.BindTo(isPaused); From 27569e2ed5f0006d9066a05cf2f5d44cb703bc8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 18:53:10 +0900 Subject: [PATCH 08/14] Remove `FrameStableClock` (and redirect usages to `FrameStabilityContainer`) --- osu.Game.Tests/NonVisual/GameplayClockTest.cs | 3 +- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- .../Rulesets/UI/FrameStabilityContainer.cs | 97 ++++++++++--------- osu.Game/Rulesets/UI/IFrameStableClock.cs | 2 - osu.Game/Screens/Play/GameplayClock.cs | 8 +- .../Screens/Play/GameplayClockContainer.cs | 2 +- osu.Game/Screens/Play/IGameplayClock.cs | 2 +- .../Play/MasterGameplayClockContainer.cs | 2 +- 9 files changed, 63 insertions(+), 57 deletions(-) diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockTest.cs index 5b8aacd281..162734f9da 100644 --- a/osu.Game.Tests/NonVisual/GameplayClockTest.cs +++ b/osu.Game.Tests/NonVisual/GameplayClockTest.cs @@ -4,6 +4,7 @@ #nullable disable using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Timing; @@ -30,7 +31,7 @@ namespace osu.Game.Tests.NonVisual { public List> MutableNonGameplayAdjustments { get; } = new List>(); - public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + public override IEnumerable NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value); public TestGameplayClock(IFrameBasedClock underlyingClock) : base(underlyingClock) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a42e86933f..0aa412a4fd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -363,7 +363,7 @@ namespace osu.Game.Tests.Visual.Gameplay private Player player => Stack.CurrentScreen as Player; private double currentFrameStableTime - => player.ChildrenOfType().First().FrameStableClock.CurrentTime; + => player.ChildrenOfType().First().CurrentTime; private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index f7f62d2af0..59c1146995 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.UI public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; - public override IFrameStableClock FrameStableClock => frameStabilityContainer.FrameStableClock; + public override IFrameStableClock FrameStableClock => frameStabilityContainer; private bool frameStablePlayback = true; diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index bfad22b4f0..7b7003302a 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; @@ -21,7 +19,9 @@ namespace osu.Game.Rulesets.UI /// A container which consumes a parent gameplay clock and standardises frame counts for children. /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks. /// - public class FrameStabilityContainer : Container, IHasReplayHandler + [Cached(typeof(IGameplayClock))] + [Cached(typeof(IFrameStableClock))] + public class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock { private readonly double gameplayStartTime; @@ -35,16 +35,17 @@ namespace osu.Game.Rulesets.UI /// internal bool FrameStablePlayback = true; - public IFrameStableClock FrameStableClock => frameStableClock; + public readonly Bindable IsCatchingUp = new Bindable(); - [Cached(typeof(IGameplayClock))] - private readonly FrameStabilityClock frameStableClock; + public readonly Bindable WaitingOnFrames = new Bindable(); + + public IBindable IsPaused { get; } = new BindableBool(); public FrameStabilityContainer(double gameplayStartTime = double.MinValue) { RelativeSizeAxes = Axes.Both; - frameStableClock = new FrameStabilityClock(framedClock = new FramedClock(manualClock = new ManualClock())); + framedClock = new FramedClock(manualClock = new ManualClock()); this.gameplayStartTime = gameplayStartTime; } @@ -53,7 +54,7 @@ namespace osu.Game.Rulesets.UI private readonly FramedClock framedClock; - private IFrameBasedClock parentGameplayClock; + private IGameplayClock? parentGameplayClock; /// /// The current direction of playback to be exposed to frame stable children. @@ -63,13 +64,13 @@ namespace osu.Game.Rulesets.UI /// private int direction = 1; - [BackgroundDependencyLoader(true)] - private void load(IGameplayClock clock) + [BackgroundDependencyLoader] + private void load(IGameplayClock? clock) { if (clock != null) { - parentGameplayClock = frameStableClock.ParentGameplayClock = clock; - ((IBindable)frameStableClock.IsPaused).BindTo(clock.IsPaused); + parentGameplayClock = clock; + IsPaused.BindTo(parentGameplayClock.IsPaused); } } @@ -111,12 +112,12 @@ namespace osu.Game.Rulesets.UI private void updateClock() { - if (frameStableClock.WaitingOnFrames.Value) + if (WaitingOnFrames.Value) { // if waiting on frames, run one update loop to determine if frames have arrived. state = PlaybackState.Valid; } - else if (frameStableClock.IsPaused.Value) + else if (IsPaused.Value) { // time should not advance while paused, nor should anything run. state = PlaybackState.NotValid; @@ -154,8 +155,8 @@ namespace osu.Game.Rulesets.UI double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); - frameStableClock.IsCatchingUp.Value = timeBehind > 200; - frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid; + IsCatchingUp.Value = timeBehind > 200; + WaitingOnFrames.Value = state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; @@ -177,6 +178,8 @@ namespace osu.Game.Rulesets.UI /// Whether playback is still valid. private bool updateReplay(ref double proposedTime) { + Debug.Assert(ReplayInputHandler != null); + double? newTime; if (FrameStablePlayback) @@ -238,18 +241,39 @@ namespace osu.Game.Rulesets.UI private void setClock() { - if (parentGameplayClock == null) - { - // in case a parent gameplay clock isn't available, just use the parent clock. - parentGameplayClock ??= Clock; - } - else - { - Clock = frameStableClock; - } + if (parentGameplayClock != null) + Clock = this; } - public ReplayInputHandler ReplayInputHandler { get; set; } + public ReplayInputHandler? ReplayInputHandler { get; set; } + + #region Delegation of IFrameStableClock + + public double CurrentTime => framedClock.CurrentTime; + + public double Rate => framedClock.Rate; + + public bool IsRunning => framedClock.IsRunning; + + public void ProcessFrame() => framedClock.ProcessFrame(); + + public double ElapsedFrameTime => framedClock.ElapsedFrameTime; + + public double FramesPerSecond => framedClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => framedClock.TimeInfo; + + #endregion + + #region Delegation of IGameplayClock + + public double TrueGameplayRate => parentGameplayClock?.TrueGameplayRate ?? 1; + + public double? StartTime => parentGameplayClock?.StartTime; + + public IEnumerable NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty(); + + #endregion private enum PlaybackState { @@ -270,24 +294,7 @@ namespace osu.Game.Rulesets.UI Valid } - private class FrameStabilityClock : GameplayClock, IFrameStableClock - { - public IGameplayClock ParentGameplayClock; - - public readonly Bindable IsCatchingUp = new Bindable(); - - public readonly Bindable WaitingOnFrames = new Bindable(); - - public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); - - public FrameStabilityClock(FramedClock underlyingClock) - : base(underlyingClock) - { - } - - IBindable IFrameStableClock.IsCatchingUp => IsCatchingUp; - - IBindable IFrameStableClock.WaitingOnFrames => WaitingOnFrames; - } + IBindable IFrameStableClock.IsCatchingUp => IsCatchingUp; + IBindable IFrameStableClock.WaitingOnFrames => WaitingOnFrames; } } diff --git a/osu.Game/Rulesets/UI/IFrameStableClock.cs b/osu.Game/Rulesets/UI/IFrameStableClock.cs index 132605adaf..569ef5e06c 100644 --- a/osu.Game/Rulesets/UI/IFrameStableClock.cs +++ b/osu.Game/Rulesets/UI/IFrameStableClock.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Framework.Timing; diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 454229fb31..b650922173 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play IBindable IGameplayClock.IsPaused => IsPaused; - public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); + public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); public GameplayClock(IFrameBasedClock underlyingClock) { @@ -46,12 +46,12 @@ namespace osu.Game.Screens.Play { double baseRate = Rate; - foreach (var adjustment in NonGameplayAdjustments) + foreach (double adjustment in NonGameplayAdjustments) { - if (Precision.AlmostEquals(adjustment.Value, 0)) + if (Precision.AlmostEquals(adjustment, 0)) return 0; - baseRate /= adjustment.Value; + baseRate /= adjustment; } return baseRate; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index df1eb32f99..27b37094ad 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Play } } - public IEnumerable> NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; + public IEnumerable NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; /// /// The final clock which is exposed to gameplay components. diff --git a/osu.Game/Screens/Play/IGameplayClock.cs b/osu.Game/Screens/Play/IGameplayClock.cs index c3d61be5d5..5f54ce691a 100644 --- a/osu.Game/Screens/Play/IGameplayClock.cs +++ b/osu.Game/Screens/Play/IGameplayClock.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play /// /// All adjustments applied to this clock which don't come from gameplay or mods. /// - IEnumerable> NonGameplayAdjustments { get; } + IEnumerable NonGameplayAdjustments { get; } IBindable IsPaused { get; } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index d7f6992fee..587d2d40a1 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -303,7 +303,7 @@ namespace osu.Game.Screens.Play private class MasterGameplayClock : GameplayClock { public readonly List> MutableNonGameplayAdjustments = new List>(); - public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + public override IEnumerable NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value); public MasterGameplayClock(FramedOffsetClock underlyingClock) : base(underlyingClock) From 828b6f2c30b779d4cee1d10c798c3b9a1353d305 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:01:28 +0900 Subject: [PATCH 09/14] Remove unnecessary `setClock` shenanigans --- .../Rulesets/UI/FrameStabilityContainer.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 7b7003302a..c115a0b6ac 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.UI protected override void LoadComplete() { base.LoadComplete(); - setClock(); + Clock = this; } private PlaybackState state; @@ -128,12 +128,7 @@ namespace osu.Game.Rulesets.UI state = PlaybackState.Valid; } - if (parentGameplayClock == null) - setClock(); // LoadComplete may not be run yet, but we still want the clock. - - Debug.Assert(parentGameplayClock != null); - - double proposedTime = parentGameplayClock.CurrentTime; + double proposedTime = Clock.CurrentTime; if (FrameStablePlayback) // if we require frame stability, the proposed time will be adjusted to move at most one known @@ -153,14 +148,14 @@ namespace osu.Game.Rulesets.UI if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; - double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); + double timeBehind = Math.Abs(proposedTime - Clock.CurrentTime); IsCatchingUp.Value = timeBehind > 200; WaitingOnFrames.Value = state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; - manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; - manualClock.IsRunning = parentGameplayClock.IsRunning; + manualClock.Rate = Math.Abs(Clock.Rate) * direction; + manualClock.IsRunning = Clock.IsRunning; // determine whether catch-up is required. if (state == PlaybackState.Valid && timeBehind > 0) @@ -239,12 +234,6 @@ namespace osu.Game.Rulesets.UI } } - private void setClock() - { - if (parentGameplayClock != null) - Clock = this; - } - public ReplayInputHandler? ReplayInputHandler { get; set; } #region Delegation of IFrameStableClock From 04d88b82163a9cf3895073784bfd53fb797451ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:17:22 +0900 Subject: [PATCH 10/14] Use constraint based assertions in `TestSceneFrameStabilityContainer` --- .../Visual/Gameplay/TestSceneFrameStabilityContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 97ffbfc796..ef74024b4b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -137,13 +137,13 @@ namespace osu.Game.Tests.Visual.Gameplay private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time); - private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime == time); + private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime, () => Is.EqualTo(time)); private void checkFrameCount(int frames) => - AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames); + AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames, () => Is.EqualTo(frames)); private void checkRate(double rate) => - AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate); + AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate, () => Is.EqualTo(rate)); public class ClockConsumingChild : CompositeDrawable { From 9bc2e91de01864e16711765869e264ade0cd1ae0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:17:41 +0900 Subject: [PATCH 11/14] Fix incorrect handling of reference clocks when no parent `IGameplayClock` is available --- .../Rulesets/UI/FrameStabilityContainer.cs | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index c115a0b6ac..411217e314 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; +using osu.Framework.Utils; using osu.Game.Input.Handlers; using osu.Game.Screens.Play; @@ -56,6 +57,8 @@ namespace osu.Game.Rulesets.UI private IGameplayClock? parentGameplayClock; + private IClock referenceClock = null!; + /// /// The current direction of playback to be exposed to frame stable children. /// @@ -65,18 +68,15 @@ namespace osu.Game.Rulesets.UI private int direction = 1; [BackgroundDependencyLoader] - private void load(IGameplayClock? clock) + private void load(IGameplayClock? gameplayClock) { - if (clock != null) + if (gameplayClock != null) { - parentGameplayClock = clock; + parentGameplayClock = gameplayClock; IsPaused.BindTo(parentGameplayClock.IsPaused); } - } - protected override void LoadComplete() - { - base.LoadComplete(); + referenceClock = gameplayClock ?? Clock; Clock = this; } @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.UI state = PlaybackState.Valid; } - double proposedTime = Clock.CurrentTime; + double proposedTime = referenceClock.CurrentTime; if (FrameStablePlayback) // if we require frame stability, the proposed time will be adjusted to move at most one known @@ -148,14 +148,14 @@ namespace osu.Game.Rulesets.UI if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; - double timeBehind = Math.Abs(proposedTime - Clock.CurrentTime); + double timeBehind = Math.Abs(proposedTime - CurrentTime); IsCatchingUp.Value = timeBehind > 200; WaitingOnFrames.Value = state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; - manualClock.Rate = Math.Abs(Clock.Rate) * direction; - manualClock.IsRunning = Clock.IsRunning; + manualClock.Rate = Math.Abs(referenceClock.Rate) * direction; + manualClock.IsRunning = referenceClock.IsRunning; // determine whether catch-up is required. if (state == PlaybackState.Valid && timeBehind > 0) @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.UI public bool IsRunning => framedClock.IsRunning; - public void ProcessFrame() => framedClock.ProcessFrame(); + public void ProcessFrame() { } public double ElapsedFrameTime => framedClock.ElapsedFrameTime; @@ -256,7 +256,23 @@ namespace osu.Game.Rulesets.UI #region Delegation of IGameplayClock - public double TrueGameplayRate => parentGameplayClock?.TrueGameplayRate ?? 1; + public double TrueGameplayRate + { + get + { + double baseRate = Rate; + + foreach (double adjustment in NonGameplayAdjustments) + { + if (Precision.AlmostEquals(adjustment, 0)) + return 0; + + baseRate /= adjustment; + } + + return baseRate; + } + } public double? StartTime => parentGameplayClock?.StartTime; From fff2b57905d75b0cb6d57f7183202e0fa80a2a4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:28:12 +0900 Subject: [PATCH 12/14] Tidy up and document `FrameStabilityContainer` --- .../Rulesets/UI/FrameStabilityContainer.cs | 97 +++++++++++-------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 411217e314..aa21b03b9d 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.UI [Cached(typeof(IFrameStableClock))] public class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock { - private readonly double gameplayStartTime; + public ReplayInputHandler? ReplayInputHandler { get; set; } /// /// The number of frames (per parent frame) which can be run in an attempt to catch-up to real-time. @@ -34,13 +34,48 @@ namespace osu.Game.Rulesets.UI /// /// Whether to enable frame-stable playback. /// - internal bool FrameStablePlayback = true; + internal bool FrameStablePlayback { get; set; } = true; - public readonly Bindable IsCatchingUp = new Bindable(); + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; - public readonly Bindable WaitingOnFrames = new Bindable(); + private readonly Bindable isCatchingUp = new Bindable(); - public IBindable IsPaused { get; } = new BindableBool(); + private readonly Bindable waitingOnFrames = new Bindable(); + + private readonly double gameplayStartTime; + + private IGameplayClock? parentGameplayClock; + + /// + /// A clock which is used as reference for time, rate and running state. + /// + private IClock referenceClock = null!; + + /// + /// A local manual clock which tracks the reference clock. + /// Values are transferred from each update call. + /// + private readonly ManualClock manualClock; + + /// + /// The main framed clock which has stability applied to it. + /// This gets exposed to children as an . + /// + private readonly FramedClock framedClock; + + /// + /// The current direction of playback to be exposed to frame stable children. + /// + /// + /// Initially it is presumed that playback will proceed in the forward direction. + /// + private int direction = 1; + + private PlaybackState state; + + private bool hasReplayAttached => ReplayInputHandler != null; + + private bool firstConsumption = true; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) { @@ -51,22 +86,6 @@ namespace osu.Game.Rulesets.UI this.gameplayStartTime = gameplayStartTime; } - private readonly ManualClock manualClock; - - private readonly FramedClock framedClock; - - private IGameplayClock? parentGameplayClock; - - private IClock referenceClock = null!; - - /// - /// The current direction of playback to be exposed to frame stable children. - /// - /// - /// Initially it is presumed that playback will proceed in the forward direction. - /// - private int direction = 1; - [BackgroundDependencyLoader] private void load(IGameplayClock? gameplayClock) { @@ -80,16 +99,6 @@ namespace osu.Game.Rulesets.UI Clock = this; } - private PlaybackState state; - - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; - - private bool hasReplayAttached => ReplayInputHandler != null; - - private const double sixty_frame_time = 1000.0 / 60; - - private bool firstConsumption = true; - public override bool UpdateSubTree() { int loops = MaxCatchUpFrames; @@ -112,7 +121,7 @@ namespace osu.Game.Rulesets.UI private void updateClock() { - if (WaitingOnFrames.Value) + if (waitingOnFrames.Value) { // if waiting on frames, run one update loop to determine if frames have arrived. state = PlaybackState.Valid; @@ -150,8 +159,8 @@ namespace osu.Game.Rulesets.UI double timeBehind = Math.Abs(proposedTime - CurrentTime); - IsCatchingUp.Value = timeBehind > 200; - WaitingOnFrames.Value = state == PlaybackState.NotValid; + isCatchingUp.Value = timeBehind > 200; + waitingOnFrames.Value = state == PlaybackState.NotValid; manualClock.CurrentTime = proposedTime; manualClock.Rate = Math.Abs(referenceClock.Rate) * direction; @@ -211,6 +220,8 @@ namespace osu.Game.Rulesets.UI /// The time which is to be displayed. private void applyFrameStability(ref double proposedTime) { + const double sixty_frame_time = 1000.0 / 60; + if (firstConsumption) { // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. @@ -234,9 +245,9 @@ namespace osu.Game.Rulesets.UI } } - public ReplayInputHandler? ReplayInputHandler { get; set; } + #region Delegation of IGameplayClock - #region Delegation of IFrameStableClock + public IBindable IsPaused { get; } = new BindableBool(); public double CurrentTime => framedClock.CurrentTime; @@ -252,10 +263,6 @@ namespace osu.Game.Rulesets.UI public FrameTimeInfo TimeInfo => framedClock.TimeInfo; - #endregion - - #region Delegation of IGameplayClock - public double TrueGameplayRate { get @@ -280,6 +287,13 @@ namespace osu.Game.Rulesets.UI #endregion + #region Delegation of IFrameStableClock + + IBindable IFrameStableClock.IsCatchingUp => isCatchingUp; + IBindable IFrameStableClock.WaitingOnFrames => waitingOnFrames; + + #endregion + private enum PlaybackState { /// @@ -298,8 +312,5 @@ namespace osu.Game.Rulesets.UI /// Valid } - - IBindable IFrameStableClock.IsCatchingUp => IsCatchingUp; - IBindable IFrameStableClock.WaitingOnFrames => WaitingOnFrames; } } From 1fc3d005c072d3d41c493de4a47e437fd544aa3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 19:31:01 +0900 Subject: [PATCH 13/14] Seal `FrameStabilityContainer` No one should ever derive from this class. It is already too complex. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index aa21b03b9d..cdb2157c4a 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.UI /// [Cached(typeof(IGameplayClock))] [Cached(typeof(IFrameStableClock))] - public class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock + public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock { public ReplayInputHandler? ReplayInputHandler { get; set; } From 87760bbc066b7e30b0b4a2986acd7f452a53b664 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Aug 2022 20:16:14 +0900 Subject: [PATCH 14/14] Fix `IsCatchingUp` not being in correct state --- .../Gameplay/TestSceneGameplaySamplePlayback.cs | 12 +++++------- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index f1084bca5f..1fe2dfd4df 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -21,22 +19,22 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestAllSamplesStopDuringSeek() { - DrawableSlider slider = null; - PoolableSkinnableSample[] samples = null; - ISamplePlaybackDisabler sampleDisabler = null; + DrawableSlider? slider = null; + PoolableSkinnableSample[] samples = null!; + ISamplePlaybackDisabler sampleDisabler = null!; AddUntilStep("get variables", () => { sampleDisabler = Player; slider = Player.ChildrenOfType().MinBy(s => s.HitObject.StartTime); - samples = slider?.ChildrenOfType().ToArray(); + samples = slider.ChildrenOfType().ToArray(); return slider != null; }); AddUntilStep("wait for slider sliding then seek", () => { - if (!slider.Tracking.Value) + if (slider?.Tracking.Value != true) return false; if (!samples.Any(s => s.Playing)) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 411217e314..e75e5cc5f3 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.UI if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; - double timeBehind = Math.Abs(proposedTime - CurrentTime); + double timeBehind = Math.Abs(proposedTime - referenceClock.CurrentTime); IsCatchingUp.Value = timeBehind > 200; WaitingOnFrames.Value = state == PlaybackState.NotValid;