From 1aa36818df11577979863233dd0a149de582504b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 17:47:11 +0900 Subject: [PATCH 1/4] Abstractify GameplayClockContainer --- .../TestSceneSpinnerRotation.cs | 3 +- .../TestSceneGameplayClockContainer.cs | 2 +- .../Gameplay/TestSceneStoryboardSamples.cs | 4 +- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 4 +- .../Screens/Play/GameplayClockContainer.cs | 277 +++++++++--------- osu.Game/Screens/Play/Player.cs | 11 +- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 2 +- 8 files changed, 156 insertions(+), 149 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 14c709cae1..8ff21057b5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Storyboards; using osu.Game.Tests.Visual; using osuTK; @@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(0); - AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate); + AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate); addSeekStep(1000); AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs index 891537c4ad..4d5dcabbba 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gcc = new GameplayClockContainer(working, 0)); + Add(gcc = new MasterGameplayClockContainer(working, 0)); }); AddStep("start track", () => gcc.Start()); diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index cae5f20332..7394458482 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gameplayContainer = new GameplayClockContainer(working, 0)); + Add(gameplayContainer = new MasterGameplayClockContainer(working, 0)); gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) { @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Gameplay var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); - Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0) + Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { Child = beatmapSkinSourceContainer }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 841722a8f1..e08e03b789 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); working.LoadTrack(); - Child = gameplayClockContainer = new GameplayClockContainer(working, 0) + Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("click", () => { - increment = skip_time - gameplayClock.CurrentTime - GameplayClockContainer.MINIMUM_SKIP_TIME / 2; + increment = skip_time - gameplayClock.CurrentTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME / 2; InputManager.Click(MouseButton.Left); }); AddStep("click", () => InputManager.Click(MouseButton.Left)); diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ddbb087962..163ed9444f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -19,27 +17,90 @@ using osu.Game.Configuration; namespace osu.Game.Screens.Play { - /// - /// Encapsulates gameplay timing logic and provides a for children. - /// - public class GameplayClockContainer : Container + public abstract class GameplayClockContainer : Container { - private readonly WorkingBeatmap beatmap; - - [NotNull] - private ITrack track; + /// + /// The final clock which is exposed to underlying components. + /// + public GameplayClock GameplayClock { get; private set; } public readonly BindableBool IsPaused = new BindableBool(); /// /// The decoupled clock used for gameplay. Should be used for seeks and clock control. /// - private readonly DecoupleableInterpolatingFramedClock adjustableClock; + protected readonly DecoupleableInterpolatingFramedClock AdjustableClock; - private readonly double gameplayStartTime; - private readonly bool startAtGameplayStart; + protected readonly IClock SourceClock; - private readonly double firstHitObjectTime; + protected GameplayClockContainer(IClock sourceClock) + { + SourceClock = sourceClock; + + RelativeSizeAxes = Axes.Both; + + AdjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + AdjustableClock.ChangeSource(SourceClock); + + IsPaused.BindValueChanged(OnPauseChanged); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableClock)); + + return dependencies; + } + + public virtual void Start() + { + if (!AdjustableClock.IsRunning) + { + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the clock source potentially taking time to enter a completely stopped state + Seek(GameplayClock.CurrentTime); + + AdjustableClock.Start(); + } + + IsPaused.Value = false; + } + + /// + /// Seek to a specific time in gameplay. + /// + /// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track). + /// + /// + /// The destination time to seek to. + public virtual void Seek(double time) => AdjustableClock.Seek(time); + + public virtual void Stop() => IsPaused.Value = true; + + public virtual void Restart() + { + AdjustableClock.Seek(0); + AdjustableClock.Stop(); + + if (!IsPaused.Value) + Start(); + } + + protected abstract void OnPauseChanged(ValueChangedEvent isPaused); + + protected abstract GameplayClock CreateGameplayClock(IClock source); + } + + public class MasterGameplayClockContainer : GameplayClockContainer + { + /// + /// Duration before gameplay start time required before skip button displays. + /// + public const double MINIMUM_SKIP_TIME = 1000; + + protected new DecoupleableInterpolatingFramedClock SourceClock => (DecoupleableInterpolatingFramedClock)base.SourceClock; public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { @@ -49,73 +110,32 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; - /// - /// The final clock which is exposed to underlying components. - /// - public GameplayClock GameplayClock => localGameplayClock; + private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; - [Cached(typeof(GameplayClock))] - private readonly LocalGameplayClock localGameplayClock; + private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); + private readonly WorkingBeatmap beatmap; + private readonly double gameplayStartTime; + private readonly bool startAtGameplayStart; + private readonly double firstHitObjectTime; + + private FramedOffsetClock userOffsetClock; + private FramedOffsetClock platformOffsetClock; + private LocalGameplayClock localGameplayClock; private Bindable userAudioOffset; - private readonly FramedOffsetClock userOffsetClock; - - private readonly FramedOffsetClock platformOffsetClock; - - /// - /// Creates a new . - /// - /// The beatmap being played. - /// The suggested time to start gameplay at. - /// - /// Whether should be used regardless of when storyboard events and hitobjects are supposed to start. - /// - public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) + : base(new DecoupleableInterpolatingFramedClock()) { this.beatmap = beatmap; this.gameplayStartTime = gameplayStartTime; this.startAtGameplayStart = startAtGameplayStart; - track = beatmap.Track; firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - RelativeSizeAxes = Axes.Both; - - adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - - // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new HardwareCorrectionOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - - // the final usable gameplay clock with user-set offsets applied. - userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); - - // the clock to be exposed via DI to children. - localGameplayClock = new LocalGameplayClock(userOffsetClock); - - GameplayClock.IsPaused.BindTo(IsPaused); - - IsPaused.BindValueChanged(onPauseChanged); + SourceClock.ChangeSource(beatmap.Track); } - private void onPauseChanged(ValueChangedEvent isPaused) - { - if (isPaused.NewValue) - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop()); - else - this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); - } - - private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; - - /// - /// Duration before gameplay start time required before skip button displays. - /// - public const double MINIMUM_SKIP_TIME = 1000; - - private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); - [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -143,39 +163,31 @@ namespace osu.Game.Screens.Play Seek(startTime); - adjustableClock.ProcessFrame(); + AdjustableClock.ProcessFrame(); } - public void Restart() + protected override void OnPauseChanged(ValueChangedEvent isPaused) { - Task.Run(() => - { - track.Seek(0); - track.Stop(); - - Schedule(() => - { - adjustableClock.ChangeSource(track); - updateRate(); - - if (!IsPaused.Value) - Start(); - }); - }); + if (isPaused.NewValue) + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableClock.Stop()); + else + this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } - public void Start() + public override void Seek(double time) { - if (!adjustableClock.IsRunning) - { - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the audio clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); + // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. + // we may want to consider reversing the application of offsets in the future as it may feel more correct. + base.Seek(time - totalOffset); - adjustableClock.Start(); - } + // manually process frame to ensure GameplayClock is correctly updated after a seek. + userOffsetClock.ProcessFrame(); + } - IsPaused.Value = false; + public override void Restart() + { + updateRate(); + base.Restart(); } /// @@ -195,26 +207,24 @@ namespace osu.Game.Screens.Play Seek(skipTarget); } - /// - /// Seek to a specific time in gameplay. - /// - /// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track). - /// - /// - /// The destination time to seek to. - public void Seek(double time) + protected override GameplayClock CreateGameplayClock(IClock source) { - // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. - // we may want to consider reversing the application of offsets in the future as it may feel more correct. - adjustableClock.Seek(time - totalOffset); + // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - // manually process frame to ensure GameplayClock is correctly updated after a seek. - userOffsetClock.ProcessFrame(); + // the final usable gameplay clock with user-set offsets applied. + userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); + + return localGameplayClock = new LocalGameplayClock(userOffsetClock); } - public void Stop() + protected override void Update() { - IsPaused.Value = true; + if (!IsPaused.Value) + userOffsetClock.ProcessFrame(); + + base.Update(); } /// @@ -223,19 +233,7 @@ namespace osu.Game.Screens.Play public void StopUsingBeatmapClock() { removeSourceClockAdjustments(); - - track = new TrackVirtual(track.Length); - adjustableClock.ChangeSource(track); - } - - protected override void Update() - { - if (!IsPaused.Value) - { - userOffsetClock.ProcessFrame(); - } - - base.Update(); + SourceClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); } private bool speedAdjustmentsApplied; @@ -245,6 +243,8 @@ namespace osu.Game.Screens.Play if (speedAdjustmentsApplied) return; + var track = (Track)SourceClock.Source; + track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); @@ -254,15 +254,12 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = true; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - removeSourceClockAdjustments(); - } - private void removeSourceClockAdjustments() { - if (!speedAdjustmentsApplied) return; + if (!speedAdjustmentsApplied) + return; + + var track = (Track)SourceClock.Source; track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); @@ -273,16 +270,10 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = false; } - private class LocalGameplayClock : GameplayClock + protected override void Dispose(bool isDisposing) { - public readonly List> MutableNonGameplayAdjustments = new List>(); - - public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; - - public LocalGameplayClock(FramedOffsetClock underlyingClock) - : base(underlyingClock) - { - } + base.Dispose(isDisposing); + removeSourceClockAdjustments(); } private class HardwareCorrectionOffsetClock : FramedOffsetClock @@ -296,5 +287,17 @@ namespace osu.Game.Screens.Play { } } + + private class LocalGameplayClock : GameplayClock + { + public readonly List> MutableNonGameplayAdjustments = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public LocalGameplayClock(FramedOffsetClock underlyingClock) + : base(underlyingClock) + { + } + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index efe5d26409..e07c40e8ff 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -295,7 +295,7 @@ namespace osu.Game.Screens.Play IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } - protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new GameplayClockContainer(beatmap, gameplayStart); + protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); private Drawable createUnderlayComponents() => DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; @@ -342,7 +342,6 @@ namespace osu.Game.Screens.Play Action = () => PerformExit(true), IsPaused = { BindTarget = GameplayClockContainer.IsPaused } }, - PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, @@ -386,6 +385,9 @@ namespace osu.Game.Screens.Play } }; + if (GameplayClockContainer is MasterGameplayClockContainer master) + HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; + if (!Configuration.AllowSkippingIntro) skipOverlay.Expire(); @@ -533,7 +535,8 @@ namespace osu.Game.Screens.Play // user requested skip // disable sample playback to stop currently playing samples and perform skip samplePlaybackDisabled.Value = true; - GameplayClockContainer.Skip(); + + (GameplayClockContainer as MasterGameplayClockContainer)?.Skip(); // return samplePlaybackDisabled.Value to what is defined by the beatmap's current state updateSampleDisabledState(); @@ -832,7 +835,7 @@ namespace osu.Game.Screens.Play // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // as we are no longer the current screen, we cannot guarantee the track is still usable. - GameplayClockContainer?.StopUsingBeatmapClock(); + (GameplayClockContainer as MasterGameplayClockContainer)?.StopUsingBeatmapClock(); musicController.ResetTrackAdjustments(); diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3f214e49d9..ddb78dfb67 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Play private const double fade_time = 300; - private double fadeOutBeginTime => startTime - GameplayClockContainer.MINIMUM_SKIP_TIME; + private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME; protected override void LoadComplete() { diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index fdf996150f..9822f62dd8 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Play if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000) return base.CreateGameplayClockContainer(beatmap, gameplayStart); - return new GameplayClockContainer(beatmap, firstFrameTime.Value, true); + return new MasterGameplayClockContainer(beatmap, firstFrameTime.Value, true); } public override bool OnExiting(IScreen next) From 2935f87e7082bd603c9a09aecda6ba51628a184b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 18:29:34 +0900 Subject: [PATCH 2/4] Fix IsPaused not being bound --- osu.Game/Screens/Play/GameplayClockContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 163ed9444f..a97a87d73f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -50,6 +50,7 @@ namespace osu.Game.Screens.Play var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableClock)); + GameplayClock.IsPaused.BindTo(IsPaused); return dependencies; } From b53b30c1a97a71822359016e87d1334ce8097476 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 19:32:48 +0900 Subject: [PATCH 3/4] Fix incorrect offset due to another intermediate Decoupleable clock --- .../Screens/Play/GameplayClockContainer.cs | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index a97a87d73f..36ca7415d0 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -31,16 +31,12 @@ namespace osu.Game.Screens.Play /// protected readonly DecoupleableInterpolatingFramedClock AdjustableClock; - protected readonly IClock SourceClock; - protected GameplayClockContainer(IClock sourceClock) { - SourceClock = sourceClock; - RelativeSizeAxes = Axes.Both; AdjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - AdjustableClock.ChangeSource(SourceClock); + AdjustableClock.ChangeSource(sourceClock); IsPaused.BindValueChanged(OnPauseChanged); } @@ -101,7 +97,7 @@ namespace osu.Game.Screens.Play /// public const double MINIMUM_SKIP_TIME = 1000; - protected new DecoupleableInterpolatingFramedClock SourceClock => (DecoupleableInterpolatingFramedClock)base.SourceClock; + protected Track Track => (Track)AdjustableClock.Source; public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { @@ -126,15 +122,13 @@ namespace osu.Game.Screens.Play private Bindable userAudioOffset; public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) - : base(new DecoupleableInterpolatingFramedClock()) + : base(beatmap.Track) { this.beatmap = beatmap; this.gameplayStartTime = gameplayStartTime; this.startAtGameplayStart = startAtGameplayStart; firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - - SourceClock.ChangeSource(beatmap.Track); } [BackgroundDependencyLoader] @@ -234,7 +228,7 @@ namespace osu.Game.Screens.Play public void StopUsingBeatmapClock() { removeSourceClockAdjustments(); - SourceClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); + AdjustableClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); } private bool speedAdjustmentsApplied; @@ -244,10 +238,8 @@ namespace osu.Game.Screens.Play if (speedAdjustmentsApplied) return; - var track = (Track)SourceClock.Source; - - track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); @@ -260,10 +252,8 @@ namespace osu.Game.Screens.Play if (!speedAdjustmentsApplied) return; - var track = (Track)SourceClock.Source; - - track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); From 18c69cdaf7bb536d22c704171ba9bd03c00689cd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 19:50:22 +0900 Subject: [PATCH 4/4] Split out files --- .../Screens/Play/GameplayClockContainer.cs | 214 +---------------- .../Play/MasterGameplayClockContainer.cs | 220 ++++++++++++++++++ 2 files changed, 222 insertions(+), 212 deletions(-) create mode 100644 osu.Game/Screens/Play/MasterGameplayClockContainer.cs diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 36ca7415d0..d8e6fda87e 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -1,19 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; -using osu.Game.Beatmaps; -using osu.Game.Configuration; namespace osu.Game.Screens.Play { @@ -38,7 +30,7 @@ namespace osu.Game.Screens.Play AdjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; AdjustableClock.ChangeSource(sourceClock); - IsPaused.BindValueChanged(OnPauseChanged); + IsPaused.BindValueChanged(OnIsPausedChanged); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -85,210 +77,8 @@ namespace osu.Game.Screens.Play Start(); } - protected abstract void OnPauseChanged(ValueChangedEvent isPaused); + protected abstract void OnIsPausedChanged(ValueChangedEvent isPaused); protected abstract GameplayClock CreateGameplayClock(IClock source); } - - public class MasterGameplayClockContainer : GameplayClockContainer - { - /// - /// Duration before gameplay start time required before skip button displays. - /// - public const double MINIMUM_SKIP_TIME = 1000; - - protected Track Track => (Track)AdjustableClock.Source; - - public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) - { - Default = 1, - MinValue = 0.5, - MaxValue = 2, - Precision = 0.1, - }; - - private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; - - private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); - - private readonly WorkingBeatmap beatmap; - private readonly double gameplayStartTime; - private readonly bool startAtGameplayStart; - private readonly double firstHitObjectTime; - - private FramedOffsetClock userOffsetClock; - private FramedOffsetClock platformOffsetClock; - private LocalGameplayClock localGameplayClock; - private Bindable userAudioOffset; - - public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) - : base(beatmap.Track) - { - this.beatmap = beatmap; - this.gameplayStartTime = gameplayStartTime; - this.startAtGameplayStart = startAtGameplayStart; - - firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); - - // sane default provided by ruleset. - double startTime = gameplayStartTime; - - if (!startAtGameplayStart) - { - startTime = Math.Min(0, startTime); - - // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. - // this is commonly used to display an intro before the audio track start. - double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; - if (firstStoryboardEvent != null) - startTime = Math.Min(startTime, firstStoryboardEvent.Value); - - // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. - // this is not available as an option in the live editor but can still be applied via .osu editing. - if (beatmap.BeatmapInfo.AudioLeadIn > 0) - startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); - } - - Seek(startTime); - - AdjustableClock.ProcessFrame(); - } - - protected override void OnPauseChanged(ValueChangedEvent isPaused) - { - if (isPaused.NewValue) - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableClock.Stop()); - else - this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); - } - - public override void Seek(double time) - { - // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. - // we may want to consider reversing the application of offsets in the future as it may feel more correct. - base.Seek(time - totalOffset); - - // manually process frame to ensure GameplayClock is correctly updated after a seek. - userOffsetClock.ProcessFrame(); - } - - public override void Restart() - { - updateRate(); - base.Restart(); - } - - /// - /// Skip forward to the next valid skip point. - /// - public void Skip() - { - if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME) - return; - - double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME; - - if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) - // double skip exception for storyboards with very long intros - skipTarget = 0; - - Seek(skipTarget); - } - - protected override GameplayClock CreateGameplayClock(IClock source) - { - // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - - // the final usable gameplay clock with user-set offsets applied. - userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); - - return localGameplayClock = new LocalGameplayClock(userOffsetClock); - } - - protected override void Update() - { - if (!IsPaused.Value) - userOffsetClock.ProcessFrame(); - - base.Update(); - } - - /// - /// Changes the backing clock to avoid using the originally provided track. - /// - public void StopUsingBeatmapClock() - { - removeSourceClockAdjustments(); - AdjustableClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); - } - - private bool speedAdjustmentsApplied; - - private void updateRate() - { - if (speedAdjustmentsApplied) - return; - - Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - - localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); - localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); - - speedAdjustmentsApplied = true; - } - - private void removeSourceClockAdjustments() - { - if (!speedAdjustmentsApplied) - return; - - Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - - localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); - localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); - - speedAdjustmentsApplied = false; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - removeSourceClockAdjustments(); - } - - private class HardwareCorrectionOffsetClock : FramedOffsetClock - { - // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. - // base implementation already adds offset at 1.0 rate, so we only add the difference from that here. - public override double CurrentTime => base.CurrentTime + Offset * (Rate - 1); - - public HardwareCorrectionOffsetClock(IClock source, bool processSource = true) - : base(source, processSource) - { - } - } - - private class LocalGameplayClock : GameplayClock - { - public readonly List> MutableNonGameplayAdjustments = new List>(); - - public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; - - public LocalGameplayClock(FramedOffsetClock underlyingClock) - : base(underlyingClock) - { - } - } - } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs new file mode 100644 index 0000000000..efc8ca732e --- /dev/null +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -0,0 +1,220 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; + +namespace osu.Game.Screens.Play +{ + public class MasterGameplayClockContainer : GameplayClockContainer + { + /// + /// Duration before gameplay start time required before skip button displays. + /// + public const double MINIMUM_SKIP_TIME = 1000; + + protected Track Track => (Track)AdjustableClock.Source; + + public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) + { + Default = 1, + MinValue = 0.5, + MaxValue = 2, + Precision = 0.1, + }; + + private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; + + private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); + + private readonly WorkingBeatmap beatmap; + private readonly double gameplayStartTime; + private readonly bool startAtGameplayStart; + private readonly double firstHitObjectTime; + + private FramedOffsetClock userOffsetClock; + private FramedOffsetClock platformOffsetClock; + private LocalGameplayClock localGameplayClock; + private Bindable userAudioOffset; + + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) + : base(beatmap.Track) + { + this.beatmap = beatmap; + this.gameplayStartTime = gameplayStartTime; + this.startAtGameplayStart = startAtGameplayStart; + + firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); + userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); + + // sane default provided by ruleset. + double startTime = gameplayStartTime; + + if (!startAtGameplayStart) + { + startTime = Math.Min(0, startTime); + + // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. + // this is commonly used to display an intro before the audio track start. + double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; + if (firstStoryboardEvent != null) + startTime = Math.Min(startTime, firstStoryboardEvent.Value); + + // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. + // this is not available as an option in the live editor but can still be applied via .osu editing. + if (beatmap.BeatmapInfo.AudioLeadIn > 0) + startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); + } + + Seek(startTime); + + AdjustableClock.ProcessFrame(); + } + + protected override void OnIsPausedChanged(ValueChangedEvent isPaused) + { + if (isPaused.NewValue) + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableClock.Stop()); + else + this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); + } + + public override void Seek(double time) + { + // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. + // we may want to consider reversing the application of offsets in the future as it may feel more correct. + base.Seek(time - totalOffset); + + // manually process frame to ensure GameplayClock is correctly updated after a seek. + userOffsetClock.ProcessFrame(); + } + + public override void Restart() + { + updateRate(); + base.Restart(); + } + + /// + /// Skip forward to the next valid skip point. + /// + public void Skip() + { + if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME) + return; + + double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME; + + if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) + // double skip exception for storyboards with very long intros + skipTarget = 0; + + Seek(skipTarget); + } + + protected override GameplayClock CreateGameplayClock(IClock source) + { + // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + + // the final usable gameplay clock with user-set offsets applied. + userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); + + return localGameplayClock = new LocalGameplayClock(userOffsetClock); + } + + protected override void Update() + { + if (!IsPaused.Value) + userOffsetClock.ProcessFrame(); + + base.Update(); + } + + /// + /// Changes the backing clock to avoid using the originally provided track. + /// + public void StopUsingBeatmapClock() + { + removeSourceClockAdjustments(); + AdjustableClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); + } + + private bool speedAdjustmentsApplied; + + private void updateRate() + { + if (speedAdjustmentsApplied) + return; + + Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + + localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); + localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); + + speedAdjustmentsApplied = true; + } + + private void removeSourceClockAdjustments() + { + if (!speedAdjustmentsApplied) + return; + + Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + + localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); + localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); + + speedAdjustmentsApplied = false; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + removeSourceClockAdjustments(); + } + + private class HardwareCorrectionOffsetClock : FramedOffsetClock + { + // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. + // base implementation already adds offset at 1.0 rate, so we only add the difference from that here. + public override double CurrentTime => base.CurrentTime + Offset * (Rate - 1); + + public HardwareCorrectionOffsetClock(IClock source, bool processSource = true) + : base(source, processSource) + { + } + } + + private class LocalGameplayClock : GameplayClock + { + public readonly List> MutableNonGameplayAdjustments = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public LocalGameplayClock(FramedOffsetClock underlyingClock) + : base(underlyingClock) + { + } + } + } +}