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.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index d1796f2231..b7f91c22f4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.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.Linq; @@ -12,7 +10,6 @@ using osu.Framework.Audio; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Objects; @@ -36,16 +33,16 @@ namespace osu.Game.Rulesets.Osu.Tests private const double spinner_duration = 6000; [Resolved] - private AudioManager audioManager { get; set; } + private AudioManager audioManager { get; set; } = null!; protected override bool Autoplay => true; protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); - private DrawableSpinner drawableSpinner; + private DrawableSpinner drawableSpinner = null!; private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single(); [SetUpSteps] @@ -67,12 +64,12 @@ namespace osu.Game.Rulesets.Osu.Tests { trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); - AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); - AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); + AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100)); addSeekStep(0); - AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); + AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance)); + AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100)); } [Test] @@ -100,20 +97,20 @@ namespace osu.Game.Rulesets.Osu.Tests // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // due to the exponential damping applied we're allowing a larger margin of error of about 10% // (5% relative to the final rotation value, but we're half-way through the spin). - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance)); + () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance)); AddAssert("symbol rotation rewound", - () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); + () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. - () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); + () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); + () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance)); AddAssert("is symbol rotation almost same", - () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); + () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); + () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); } [Test] @@ -177,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value); addSeekStep(2000); - AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0)); addSeekStep(1000); - AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0)); + AddAssert("spm still valid", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(estimatedSpm).Within(1.0)); } [TestCase(0.5)] @@ -202,14 +199,14 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate); addSeekStep(1000); - AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); - AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); + AddAssert("progress almost same", () => expectedProgress, () => Is.EqualTo(drawableSpinner.Progress).Within(0.05)); + AddAssert("spm almost same", () => expectedSpm, () => Is.EqualTo(drawableSpinner.SpinsPerMinute.Value).Within(2.0)); } private void addSeekStep(double time) { AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddUntilStep("wait for seek to finish", () => time, () => Is.EqualTo(Player.DrawableRuleset.FrameStableClock.CurrentTime).Within(100)); } private void transformReplay(Func replayTransformation) => AddStep("set replay", () => 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/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..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)) @@ -141,7 +139,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/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/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 96ba802a5f..3c6820e49b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -65,7 +63,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); AddUntilStep("clock stops", () => !Clock.IsRunning); - AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); + AddUntilStep("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); AddStep("start clock again", () => Clock.Start()); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); @@ -80,7 +78,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("clock stopped", () => !Clock.IsRunning); AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); - AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); + AddAssert("clock stopped at end", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); AddStep("start clock again", () => Clock.Start()); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); @@ -92,16 +90,16 @@ namespace osu.Game.Tests.Visual.Editing AddStep("stop clock", () => Clock.Stop()); AddStep("seek before start time", () => Clock.Seek(-1000)); - AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); + AddAssert("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0)); AddStep("seek beyond track length", () => Clock.Seek(Clock.TrackLength + 1000)); - AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength); + AddAssert("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); AddStep("seek smoothly before start time", () => Clock.SeekSmoothlyTo(-1000)); - AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); + AddUntilStep("time is clamped to 0", () => Clock.CurrentTime, () => Is.EqualTo(0)); AddStep("seek smoothly beyond track length", () => Clock.SeekSmoothlyTo(Clock.TrackLength + 1000)); - AddAssert("time is clamped to track length", () => Clock.CurrentTime == Clock.TrackLength); + AddUntilStep("time is clamped to track length", () => Clock.CurrentTime, () => Is.EqualTo(Clock.TrackLength)); } protected override void Dispose(bool isDisposing) 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/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 { 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.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index fb97f94dbb..3e2698bc05 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -38,8 +38,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + [Cached(typeof(IGameplayClock))] + 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/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/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 05474e3d39..dbbedf37ae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -56,6 +56,10 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly ChangelogOverlay changelogOverlay; + private double savedTrackVolume; + private double savedMasterVolume; + private bool savedMutedState; + public TestScenePlayerLoader() { AddRange(new Drawable[] @@ -75,11 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUp] - public void Setup() => Schedule(() => - { - player = null; - audioManager.Volume.SetDefault(); - }); + public void Setup() => Schedule(() => player = null); /// /// Sets the input manager child to a new test player loader container instance. @@ -147,6 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay moveMouse(); return player?.LoadState == LoadState.Ready; }); + AddRepeatStep("move mouse", moveMouse, 20); AddAssert("loader still active", () => loader.IsCurrentScreen()); @@ -154,6 +155,8 @@ namespace osu.Game.Tests.Visual.Gameplay void moveMouse() { + notificationOverlay.State.Value = Visibility.Hidden; + InputManager.MoveMouseTo( loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) @@ -274,6 +277,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("load player", () => resetPlayer(false, beforeLoad)); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); + saveVolumes(); + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); AddStep("click notification", () => { @@ -287,6 +292,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("check " + volumeName, assert); + restoreVolumes(); + AddUntilStep("wait for player load", () => player.IsLoaded); } @@ -294,6 +301,9 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(false)] public void TestEpilepsyWarning(bool warning) { + saveVolumes(); + setFullVolume(); + AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("load dummy beatmap", () => resetPlayer(false)); @@ -306,6 +316,30 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25); AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } + + restoreVolumes(); + } + + [Test] + public void TestEpilepsyWarningEarlyExit() + { + saveVolumes(); + setFullVolume(); + + AddStep("set epilepsy warning", () => epilepsyWarning = true); + AddStep("load dummy beatmap", () => resetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); + AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); + + AddStep("exit early", () => loader.Exit()); + + AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); + AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + + restoreVolumes(); } [TestCase(true, 1.0, false)] // on battery, above cutoff --> no warning @@ -336,21 +370,34 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for player load", () => player.IsLoaded); } - [Test] - public void TestEpilepsyWarningEarlyExit() + private void restoreVolumes() { - AddStep("set epilepsy warning", () => epilepsyWarning = true); - AddStep("load dummy beatmap", () => resetPlayer(false)); + AddStep("restore previous volumes", () => + { + audioManager.VolumeTrack.Value = savedTrackVolume; + audioManager.Volume.Value = savedMasterVolume; + volumeOverlay.IsMuted.Value = savedMutedState; + }); + } - AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + private void setFullVolume() + { + AddStep("set volumes to 100%", () => + { + audioManager.VolumeTrack.Value = 1; + audioManager.Volume.Value = 1; + volumeOverlay.IsMuted.Value = false; + }); + } - AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); - AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible); - - AddStep("exit early", () => loader.Exit()); - - AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); - AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + private void saveVolumes() + { + AddStep("save previous volumes", () => + { + savedTrackVolume = audioManager.VolumeTrack.Value; + savedMasterVolume = audioManager.Volume.Value; + savedMutedState = volumeOverlay.IsMuted.Value; + }); } private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 3eb92b3e97..e29101ba8d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -29,8 +29,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + [Cached(typeof(IGameplayClock))] + 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..00e4171eac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -36,8 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); - [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + [Cached(typeof(IGameplayClock))] + private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock()); private IEnumerable hudOverlays => CreatedDrawables.OfType(); 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..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.GameplayClock); + Dependencies.CacheAs(gameplayClockContainer); } [SetUpSteps] 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.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/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 dcd7141419..e41802808f 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -1,16 +1,16 @@ // 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; using System.Linq; using osu.Framework.Allocation; 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; @@ -20,9 +20,11 @@ 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 sealed 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. @@ -32,28 +34,35 @@ namespace osu.Game.Rulesets.UI /// /// Whether to enable frame-stable playback. /// - internal bool FrameStablePlayback = true; + internal bool FrameStablePlayback { get; set; } = true; - public IFrameStableClock FrameStableClock => frameStableClock; + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; - [Cached(typeof(GameplayClock))] - private readonly FrameStabilityClock frameStableClock; + private readonly Bindable isCatchingUp = new Bindable(); - public FrameStabilityContainer(double gameplayStartTime = double.MinValue) - { - RelativeSizeAxes = Axes.Both; + private readonly Bindable waitingOnFrames = new Bindable(); - frameStableClock = new FrameStabilityClock(framedClock = new FramedClock(manualClock = new ManualClock())); + private readonly double gameplayStartTime; - this.gameplayStartTime = 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; - private IFrameBasedClock parentGameplayClock; - /// /// The current direction of playback to be exposed to frame stable children. /// @@ -62,32 +71,34 @@ namespace osu.Game.Rulesets.UI /// private int direction = 1; - [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock) - { - if (clock != null) - { - parentGameplayClock = frameStableClock.ParentGameplayClock = clock; - frameStableClock.IsPaused.BindTo(clock.IsPaused); - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - setClock(); - } - 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 FrameStabilityContainer(double gameplayStartTime = double.MinValue) + { + RelativeSizeAxes = Axes.Both; + + framedClock = new FramedClock(manualClock = new ManualClock()); + + this.gameplayStartTime = gameplayStartTime; + } + + [BackgroundDependencyLoader] + private void load(IGameplayClock? gameplayClock) + { + if (gameplayClock != null) + { + parentGameplayClock = gameplayClock; + IsPaused.BindTo(parentGameplayClock.IsPaused); + } + + referenceClock = gameplayClock ?? Clock; + Clock = this; + } + public override bool UpdateSubTree() { int loops = MaxCatchUpFrames; @@ -110,12 +121,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; @@ -126,10 +137,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. - - double proposedTime = parentGameplayClock.CurrentTime; + double proposedTime = referenceClock.CurrentTime; if (FrameStablePlayback) // if we require frame stability, the proposed time will be adjusted to move at most one known @@ -149,14 +157,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 - referenceClock.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; - manualClock.IsRunning = parentGameplayClock.IsRunning; + manualClock.Rate = Math.Abs(referenceClock.Rate) * direction; + manualClock.IsRunning = referenceClock.IsRunning; // determine whether catch-up is required. if (state == PlaybackState.Valid && timeBehind > 0) @@ -174,6 +182,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) @@ -210,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. @@ -233,20 +245,54 @@ namespace osu.Game.Rulesets.UI } } - private void setClock() + #region Delegation of IGameplayClock + + public IBindable IsPaused { get; } = new BindableBool(); + + public double CurrentTime => framedClock.CurrentTime; + + public double Rate => framedClock.Rate; + + public bool IsRunning => framedClock.IsRunning; + + public void ProcessFrame() { } + + public double ElapsedFrameTime => framedClock.ElapsedFrameTime; + + public double FramesPerSecond => framedClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => framedClock.TimeInfo; + + public double TrueGameplayRate { - if (parentGameplayClock == null) + get { - // in case a parent gameplay clock isn't available, just use the parent clock. - parentGameplayClock ??= Clock; - } - else - { - Clock = frameStableClock; + double baseRate = Rate; + + foreach (double adjustment in NonGameplayAdjustments) + { + if (Precision.AlmostEquals(adjustment, 0)) + return 0; + + baseRate /= adjustment; + } + + return baseRate; } } - public ReplayInputHandler ReplayInputHandler { get; set; } + public double? StartTime => parentGameplayClock?.StartTime; + + public IEnumerable NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty(); + + #endregion + + #region Delegation of IFrameStableClock + + IBindable IFrameStableClock.IsCatchingUp => isCatchingUp; + IBindable IFrameStableClock.WaitingOnFrames => waitingOnFrames; + + #endregion private enum PlaybackState { @@ -266,25 +312,5 @@ namespace osu.Game.Rulesets.UI /// Valid } - - private class FrameStabilityClock : GameplayClock, IFrameStableClock - { - public GameplayClock 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; - } } } 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/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/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/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..b650922173 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -19,51 +19,39 @@ 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. - /// - public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); + IBindable IGameplayClock.IsPaused => IsPaused; + + public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); public GameplayClock(IFrameBasedClock underlyingClock) { 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 { 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 b37d15e06c..27b37094ad 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -1,11 +1,11 @@ // 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 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,17 +16,14 @@ 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, IGameplayClock { - /// - /// The final clock which is exposed to gameplay components. - /// - public GameplayClock GameplayClock { get; private set; } - /// /// 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. @@ -36,12 +33,12 @@ 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 /// - public event Action OnSeek; + public event Action? OnSeek; private double? startTime; @@ -59,16 +56,23 @@ namespace osu.Game.Screens.Play { startTime = value; - if (GameplayClock != null) + if (GameplayClock.IsNotNull()) GameplayClock.StartTime = value; } } + public IEnumerable NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; + + /// + /// The final clock which is exposed to gameplay components. + /// + protected GameplayClock GameplayClock { get; private set; } = null!; + /// /// Creates a new . /// /// The source used for timing. - protected GameplayClockContainer(IClock sourceClock) + public GameplayClockContainer(IClock sourceClock) { SourceClock = sourceClock; @@ -82,10 +86,12 @@ 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); + GameplayClock.IsPaused.BindTo(isPaused); return dependencies; } @@ -106,7 +112,7 @@ namespace osu.Game.Screens.Play AdjustableSource.Start(); } - IsPaused.Value = false; + isPaused.Value = false; } /// @@ -128,7 +134,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. @@ -193,7 +199,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 @@ -205,9 +211,7 @@ namespace osu.Game.Screens.Play void IAdjustableClock.Reset() => Reset(); - public void ResetSpeedAdjustments() - { - } + public void ResetSpeedAdjustments() => throw new NotImplementedException(); double IAdjustableClock.Rate { @@ -215,12 +219,25 @@ 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; + + public double TrueGameplayRate => GameplayClock.TrueGameplayRate; } } diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 09afd7a9d3..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; } @@ -94,7 +94,10 @@ namespace osu.Game.Screens.Play.HUD double objectOffsetCurrent = currentTime - FirstHitTime; double objectDuration = LastHitTime - FirstHitTime; - UpdateProgress(objectOffsetCurrent / objectDuration, false); + if (objectDuration == 0) + UpdateProgress(0, false); + else + UpdateProgress(objectOffsetCurrent / objectDuration, false); } } } 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..5f54ce691a --- /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/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) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 08b6da1921..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 => { @@ -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); 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(); 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;