From 600651795b88563e77e5e8b3120aa71367329aee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 15:23:22 +0900 Subject: [PATCH 01/62] Change `FramedBeatmapClock` to always be decoupled --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 10 +++------- osu.Game/Screens/Edit/EditorClock.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 62484fa12b..1626fec9bd 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -13,6 +13,8 @@ using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Screens.Play; +#pragma warning disable CS0618 + namespace osu.Game.Beatmaps { /// @@ -66,19 +68,13 @@ namespace osu.Game.Beatmaps public bool IsRewinding { get; private set; } - public bool IsCoupled - { - get => decoupledClock.IsCoupled; - set => decoupledClock.IsCoupled = value; - } - public FramedBeatmapClock(bool applyOffsets = false) { this.applyOffsets = applyOffsets; // A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). - decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; + decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; if (applyOffsets) { diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index a05a873101..108552b61c 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); - underlyingClock = new FramedBeatmapClock(applyOffsets: true) { IsCoupled = false }; + underlyingClock = new FramedBeatmapClock(applyOffsets: true); AddInternal(underlyingClock); } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 2478af1dd4..6e069297d4 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play InternalChildren = new Drawable[] { - GameplayClock = new FramedBeatmapClock(applyOffsets) { IsCoupled = false }, + GameplayClock = new FramedBeatmapClock(applyOffsets), Content }; } From 3f27be1f330103ced3937de808107376b350bcd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 15:24:03 +0900 Subject: [PATCH 02/62] Replace most usages of `DecoupleableInterpolatingFramedClock` Except `FramedBeatmapClock`, which is the high-effort one. --- .../Visual/Gameplay/TestSceneStoryboard.cs | 4 +--- osu.Game/Screens/Menu/IntroTriangles.cs | 13 +++---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index a6663f3086..893b9f11f4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -106,14 +106,12 @@ namespace osu.Game.Tests.Visual.Gameplay if (storyboard != null) storyboardContainer.Remove(storyboard, true); - var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; - storyboardContainer.Clock = decoupledClock; + storyboardContainer.Clock = new FramedClock(Beatmap.Value.Track); storyboard = toLoad.CreateDrawable(SelectedMods.Value); storyboard.Passing = false; storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(Beatmap.Value.Track); } private void loadStoryboard(string filename, Action? setUpStoryboard = null) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index a9c86b10c4..808680b9e5 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -36,7 +36,6 @@ namespace osu.Game.Screens.Menu private Sample welcome; - private DecoupleableInterpolatingFramedClock decoupledClock; private TrianglesIntroSequence intro; public IntroTriangles([CanBeNull] Func createNextScreen = null) @@ -59,18 +58,12 @@ namespace osu.Game.Screens.Menu { PrepareMenuLoad(); - decoupledClock = new DecoupleableInterpolatingFramedClock - { - IsCoupled = false - }; - - if (UsingThemedIntro) - decoupledClock.ChangeSource(Track); + var decouplingClock = new DecouplingClock(UsingThemedIntro ? Track : null); LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground()) { RelativeSizeAxes = Axes.Both, - Clock = decoupledClock, + Clock = new InterpolatingFramedClock(decouplingClock), LoadMenu = LoadMenu }, _ => { @@ -94,7 +87,7 @@ namespace osu.Game.Screens.Menu StartTrack(); // no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure. - decoupledClock.Start(); + decouplingClock.Start(); }); } } From 117cd74af671877a05600e261a93323a39fcd7e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 15:32:29 +0900 Subject: [PATCH 03/62] Update usage of `DecoupleableInterpolatingFramedClock` in `FramedBeatmapClock` --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 33 +++++++++---------- .../Screens/Play/GameplayClockContainer.cs | 3 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 1626fec9bd..acd10cccf3 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -13,8 +13,6 @@ using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Screens.Play; -#pragma warning disable CS0618 - namespace osu.Game.Beatmaps { /// @@ -55,7 +53,7 @@ namespace osu.Game.Beatmaps private IDisposable? beatmapOffsetSubscription; - private readonly DecoupleableInterpolatingFramedClock decoupledClock; + private readonly DecouplingClock decoupledTrack; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -74,13 +72,13 @@ namespace osu.Game.Beatmaps // A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). - decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + decoupledTrack = new DecouplingClock(); if (applyOffsets) { // Audio timings in general with newer BASS versions don't match stable. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(decoupledClock, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new OffsetCorrectionClock(decoupledTrack, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // User global offset (set in settings) should also be applied. userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust); @@ -90,7 +88,7 @@ namespace osu.Game.Beatmaps } else { - finalClockSource = decoupledClock; + finalClockSource = new InterpolatingFramedClock(decoupledTrack); } } @@ -120,13 +118,12 @@ namespace osu.Game.Beatmaps { base.Update(); - if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime - 100) + // TODO: necessary? + if (Source.CurrentTime < decoupledTrack.CurrentTime - 100) { // InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime. // See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93 // This is not always the case here when doing large seeks. - // (Of note, this is not an issue if the source is adjustable, as the source is seeked to be in time by DecoupleableInterpolatingFramedClock). - // Rather than trying to get around this by fixing the framework clock stack, let's work around it for now. Seek(Source.CurrentTime); } else @@ -156,43 +153,43 @@ namespace osu.Game.Beatmaps public void ChangeSource(IClock? source) { Track = source as Track ?? new TrackVirtual(60000); - decoupledClock.ChangeSource(source); + decoupledTrack.ChangeSource(Track); } - public IClock? Source => decoupledClock.Source; + public IClock Source => decoupledTrack.Source; public void Reset() { - decoupledClock.Reset(); + decoupledTrack.Reset(); finalClockSource.ProcessFrame(); } public void Start() { - decoupledClock.Start(); + decoupledTrack.Start(); finalClockSource.ProcessFrame(); } public void Stop() { - decoupledClock.Stop(); + decoupledTrack.Stop(); finalClockSource.ProcessFrame(); } public bool Seek(double position) { - bool success = decoupledClock.Seek(position - TotalAppliedOffset); + bool success = decoupledTrack.Seek(position - TotalAppliedOffset); finalClockSource.ProcessFrame(); return success; } - public void ResetSpeedAdjustments() => decoupledClock.ResetSpeedAdjustments(); + public void ResetSpeedAdjustments() => decoupledTrack.ResetSpeedAdjustments(); public double Rate { - get => decoupledClock.Rate; - set => decoupledClock.Rate = value; + get => decoupledTrack.Rate; + set => decoupledTrack.Rate = value; } #endregion diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6e069297d4..ba034b079f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -194,7 +194,8 @@ namespace osu.Game.Screens.Play /// private void ensureSourceClockSet() { - if (GameplayClock.Source == null) + // TODO: does this need to exist? + if (GameplayClock.Source != SourceClock) ChangeSource(SourceClock); } From 04e6ec87158a0117f366b4ef8dc8289b18a6e285 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 17:02:31 +0900 Subject: [PATCH 04/62] Fix interpolation not being applied when `applyOffsets` is set --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index acd10cccf3..338a5d3056 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -74,11 +74,13 @@ namespace osu.Game.Beatmaps // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). decoupledTrack = new DecouplingClock(); + var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack); + if (applyOffsets) { // Audio timings in general with newer BASS versions don't match stable. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new OffsetCorrectionClock(decoupledTrack, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // User global offset (set in settings) should also be applied. userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust); @@ -88,7 +90,7 @@ namespace osu.Game.Beatmaps } else { - finalClockSource = new InterpolatingFramedClock(decoupledTrack); + finalClockSource = new InterpolatingFramedClock(interpolatedTrack); } } From 21a2e27e5fff1adfdb3ac8eae52804919ac987a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 18:39:55 +0900 Subject: [PATCH 05/62] Simplify some pieces of `FramedBeatmapClock` --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 338a5d3056..7a12528453 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -28,16 +28,6 @@ namespace osu.Game.Beatmaps { private readonly bool applyOffsets; - /// - /// The length of the underlying beatmap track. Will default to 60 seconds if unavailable. - /// - public double TrackLength => Track.Length; - - /// - /// The underlying beatmap track, if available. - /// - public Track Track { get; private set; } = new TrackVirtual(60000); - /// /// The total frequency adjustment from pause transforms. Should eventually be handled in a better way. /// @@ -154,8 +144,7 @@ namespace osu.Game.Beatmaps public void ChangeSource(IClock? source) { - Track = source as Track ?? new TrackVirtual(60000); - decoupledTrack.ChangeSource(Track); + decoupledTrack.ChangeSource(source as Track ?? new TrackVirtual(60000)); } public IClock Source => decoupledTrack.Source; From 5f634f2812b42c6e6d404b19c2dc015db80b3575 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 18:45:34 +0900 Subject: [PATCH 06/62] Remove unnecessary encapsulation workaround The new implementation of `DecouplingClock` will not mutate the underlying clock in any way (unless attempting to start it when approaching from a negative time value). This should be quite safe as a result. --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 5 +---- osu.Game/OsuGameBase.cs | 11 +---------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 7a12528453..cd349e6c29 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -142,10 +142,7 @@ namespace osu.Game.Beatmaps #region Delegation of IAdjustableClock / ISourceChangeableClock to decoupled clock. - public void ChangeSource(IClock? source) - { - decoupledTrack.ChangeSource(source as Track ?? new TrackVirtual(60000)); - } + public void ChangeSource(IClock? source) => decoupledTrack.ChangeSource(source); public IClock Source => decoupledTrack.Source; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 75b46a0a4d..c946362124 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -441,16 +441,7 @@ namespace osu.Game } } - private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) - { - // FramedBeatmapClock uses a decoupled clock internally which will mutate the source if it is an `IAdjustableClock`. - // We don't want this for now, as the intention of beatmapClock is to be a read-only source for beat sync components. - // - // Encapsulating in a FramedClock will avoid any mutations. - var framedClock = new FramedClock(beatmap.Track); - - beatmapClock.ChangeSource(framedClock); - } + private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track); protected virtual void InitialiseFonts() { From df08c4e1adf701068e9c00dd2e2fd40d248c9bb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 18:49:44 +0900 Subject: [PATCH 07/62] Disable decoupling for `OsuGameBase`'s beatmap implementation This avoids it ever mutating the underlying track (aka attempting to start it). Resolves the one caveat mentioned in aeef92fa710648d4a00edc523e13c17ac6104125. --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 8 ++++---- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Screens/Edit/EditorClock.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index cd349e6c29..d738333a98 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -56,14 +56,14 @@ namespace osu.Game.Beatmaps public bool IsRewinding { get; private set; } - public FramedBeatmapClock(bool applyOffsets = false) + public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling) { this.applyOffsets = applyOffsets; - // A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting - // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). - decoupledTrack = new DecouplingClock(); + decoupledTrack = new DecouplingClock { AllowDecoupling = requireDecoupling }; + // An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting + // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack); if (applyOffsets) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index c946362124..1f46eb0c0d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -215,7 +215,7 @@ namespace osu.Game /// For now, this is used as a source specifically for beat synced components. /// Going forward, it could potentially be used as the single source-of-truth for beatmap timing. /// - private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(true); + private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: false); protected override Container Content => content; diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 108552b61c..d4de1bae5f 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); - underlyingClock = new FramedBeatmapClock(applyOffsets: true); + underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true); AddInternal(underlyingClock); } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ba034b079f..2a27d33047 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play InternalChildren = new Drawable[] { - GameplayClock = new FramedBeatmapClock(applyOffsets), + GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling: true), Content }; } From a451ab75dd4a72dcce21428ffffb27f4cf26cdc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 19:10:14 +0900 Subject: [PATCH 08/62] Remove hopefully-unnecessary workaround It was causing issues with the new implementation. --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index d738333a98..c83874ded3 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -5,7 +5,6 @@ using System; using System.Diagnostics; using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Timing; @@ -110,16 +109,7 @@ namespace osu.Game.Beatmaps { base.Update(); - // TODO: necessary? - if (Source.CurrentTime < decoupledTrack.CurrentTime - 100) - { - // InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime. - // See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93 - // This is not always the case here when doing large seeks. - Seek(Source.CurrentTime); - } - else - finalClockSource.ProcessFrame(); + finalClockSource.ProcessFrame(); if (Clock.ElapsedFrameTime != 0) IsRewinding = Clock.ElapsedFrameTime < 0; From a1e298930c6a0e697f9f3f2cf80be6d2c3b3704c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 19:39:15 +0900 Subject: [PATCH 09/62] Remove second hopefully-unnecessary workaround --- osu.Game/Screens/Play/GameplayClockContainer.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 2a27d33047..4c3aa301a6 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -160,21 +160,6 @@ namespace osu.Game.Screens.Play Seek(StartTime); - // This is a workaround for the fact that DecoupleableInterpolatingFramedClock doesn't seek the source - // if the source is not IsRunning. (see https://github.com/ppy/osu-framework/blob/2102638056dfcf85d21b4d85266d53b5dd018767/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs#L209-L210) - // I hope to remove this once we knock some sense into clocks in general. - // - // Without this seek, the multiplayer spectator start sequence breaks: - // - Individual clients' clocks are never updated to their expected time - // - The sync manager thinks they are running behind - // - Gameplay doesn't start when it should (until a timeout occurs because nothing is happening for 10+ seconds) - // - // In addition, we use `CurrentTime` for this seek instead of `StartTime` as the above seek may have applied inherent - // offsets which need to be accounted for (ie. FramedBeatmapClock.TotalAppliedOffset). - // - // See https://github.com/ppy/osu/pull/24451/files/87fee001c786b29db34063ef3350e9a9f024d3ab#diff-28ca02979641e2d98a15fe5d5e806f56acf60ac100258a059fa72503b6cc54e8. - (SourceClock as IAdjustableClock)?.Seek(CurrentTime); - if (!wasPaused || startClock) Start(); } From 0200b63fd385eb8ed42b480d86d09e13e5b152e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Sep 2023 19:53:55 +0900 Subject: [PATCH 10/62] Add note about beatmap offset not being reapplied correctly on `ChangeSource` --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index c83874ded3..a3e1b9ae24 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -95,6 +95,7 @@ namespace osu.Game.Beatmaps userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); + // TODO: this doesn't update when using ChangeSource() to change beatmap. beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, settings => settings.Offset, From 59d6e6751255558aab0cbdba7de97105437dd8b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 04:10:57 +0900 Subject: [PATCH 11/62] Add missing `TestManualClock.Reset` implementation for safe measure --- osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 1d568a9dc2..63ebe5187f 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -188,6 +188,8 @@ namespace osu.Game.Tests.OnlinePlay public void Reset() { + IsRunning = false; + CurrentTime = 0; } public void ResetSpeedAdjustments() From 586311d508c28ffb486de78e5e490639714b746a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 13:16:51 +0900 Subject: [PATCH 12/62] Fix souce clock not always being transferred to `FramedBeatmapClock` in time --- .../Spectate/MultiSpectatorPlayer.cs | 2 +- .../Screens/Play/GameplayClockContainer.cs | 29 ++----------------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 930bea4497..18a890c2b8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate 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. - if (GameplayClockContainer.SourceClock.IsRunning) + if (GameplayClockContainer.IsRunning) GameplayClockContainer.Start(); else GameplayClockContainer.Stop(); diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 4c3aa301a6..38e8fe0dc7 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -23,11 +23,6 @@ namespace osu.Game.Screens.Play public bool IsRewinding => GameplayClock.IsRewinding; - /// - /// The source clock. Should generally not be used for any timekeeping purposes. - /// - public IClock SourceClock { get; private set; } - /// /// Invoked when a seek has been performed via /// @@ -62,8 +57,6 @@ namespace osu.Game.Screens.Play /// Whether to apply platform, user and beatmap offsets to the mix. public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false) { - SourceClock = sourceClock; - RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] @@ -71,6 +64,8 @@ namespace osu.Game.Screens.Play GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling: true), Content }; + + GameplayClock.ChangeSource(sourceClock); } /// @@ -83,8 +78,6 @@ namespace osu.Game.Screens.Play isPaused.Value = false; - ensureSourceClockSet(); - PrepareStart(); // The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time. @@ -153,8 +146,6 @@ namespace osu.Game.Screens.Play Stop(); - ensureSourceClockSet(); - if (time != null) StartTime = time.Value; @@ -168,21 +159,7 @@ namespace osu.Game.Screens.Play /// Changes the source clock. /// /// The new source. - protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(SourceClock = sourceClock); - - /// - /// Ensures that the is set to , if it hasn't been given a source yet. - /// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode, - /// but not the actual source clock. - /// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor, - /// but it is not yet set on the adjustable source there. - /// - private void ensureSourceClockSet() - { - // TODO: does this need to exist? - if (GameplayClock.Source != SourceClock) - ChangeSource(SourceClock); - } + protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(sourceClock); #region IAdjustableClock From bf08fbe1962b613597fad898a5c75ee0baa9530f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 13:29:46 +0900 Subject: [PATCH 13/62] Set source directly in `FramedBeatmapClock` ctor This isn't required, but avoids creating a temporary `StopwatchClock` and generally just makes debug easier with less state changes. --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 4 ++-- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index a3e1b9ae24..495a30b87c 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -55,11 +55,11 @@ namespace osu.Game.Beatmaps public bool IsRewinding { get; private set; } - public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling) + public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? source = null) { this.applyOffsets = applyOffsets; - decoupledTrack = new DecouplingClock { AllowDecoupling = requireDecoupling }; + decoupledTrack = new DecouplingClock(source) { AllowDecoupling = requireDecoupling }; // An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 38e8fe0dc7..ace4778ac7 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -61,11 +61,9 @@ namespace osu.Game.Screens.Play InternalChildren = new Drawable[] { - GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling: true), + GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling: true, sourceClock), Content }; - - GameplayClock.ChangeSource(sourceClock); } /// From 3cb928fe6f4d09e43bd0336d22e53d90dde4c4cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 14:06:05 +0900 Subject: [PATCH 14/62] Add note about test not calling `ProcessFrame` --- osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index 63ebe5187f..d601f187d8 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -15,6 +15,9 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.OnlinePlay { + // NOTE: This test scene never calls ProcessFrame on clocks. + // The current tests are fine without this as they are testing very static scenarios, but it's worth knowing + // if adding further tests to this class. [HeadlessTest] public partial class TestSceneCatchUpSyncManager : OsuTestScene { From 6629a47ed38daff0356122f61d8676037e7e4319 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 14:10:26 +0900 Subject: [PATCH 15/62] Fix `FramedBeatmapClock`'s source not being processed --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 495a30b87c..5fdabaab46 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -110,6 +110,9 @@ namespace osu.Game.Beatmaps { base.Update(); + if (decoupledTrack.Source is IFrameBasedClock framedClock) + framedClock.ProcessFrame(); + finalClockSource.ProcessFrame(); if (Clock.ElapsedFrameTime != 0) From 8367bb6bee4ea92dc97771eb343f056cdd60d45d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 14:26:33 +0900 Subject: [PATCH 16/62] Don't apply decoupling to `SpectatorPlayerClock`s See inline comment for reasoning. It's a bit complicated. --- .../TestSceneDrumSampleTriggerSource.cs | 2 +- osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs | 2 +- osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- .../Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs | 2 +- .../Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 2 +- .../OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs | 5 ++++- osu.Game/Screens/Play/GameplayClockContainer.cs | 5 +++-- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 2 +- 9 files changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs index ced2e4b98c..6c925f566b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumSampleTriggerSource.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [SetUp] public void SetUp() => Schedule(() => { - gameplayClock = new GameplayClockContainer(manualClock) + gameplayClock = new GameplayClockContainer(manualClock, false, false) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs index d67a3cb824..4b3163ad89 100644 --- a/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs +++ b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.NonVisual private partial class TestGameplayClockContainer : GameplayClockContainer { public TestGameplayClockContainer(IFrameBasedClock underlyingClock) - : base(underlyingClock) + : base(underlyingClock, false, false) { AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(2.0)); } diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index d601f187d8..7b0b211899 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.OnlinePlay [SetUp] public void Setup() { - syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock())); + syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock(), false, false)); player1 = syncManager.CreateManagedClock(); player2 = syncManager.CreateManagedClock(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index aa89342926..22600172dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock(), false, false); // best way to check without exposing. private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 81ce088b9d..f1e9c831a6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock(), false, false); [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index a8580ebf77..782cbb0a85 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock(), false, false); private IEnumerable hudOverlays => CreatedDrawables.OfType(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 18a890c2b8..8526e11e12 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -67,7 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { - var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock); + // Importantly, we don't want to apply decoupling because SpectatorPlayerClock updates its IsRunning directly. + // If we applied decoupling, this state change wouldn't actually cause the clock to stop. + // TODO: Can we just use Start/Stop rather than this workaround, now that DecouplingClock is more sane? + var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock, applyOffsets: false, requireDecoupling: false); clockAdjustmentsFromMods.BindAdjustments(gameplayClockContainer.AdjustmentsFromMods); return gameplayClockContainer; } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ace4778ac7..e6a38a9946 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -55,13 +55,14 @@ namespace osu.Game.Screens.Play /// /// The source used for timing. /// Whether to apply platform, user and beatmap offsets to the mix. - public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false) + /// Whether decoupling logic should be applied on the source clock. + public GameplayClockContainer(IClock sourceClock, bool applyOffsets, bool requireDecoupling) { RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { - GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling: true, sourceClock), + GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling, sourceClock), Content }; } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 489a4ef8b3..9ecabd9c6b 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play /// The beatmap to be used for time and metadata references. /// The latest time which should be used when introducing gameplay. Will be used when skipping forward. public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime) - : base(beatmap.Track, true) + : base(beatmap.Track, applyOffsets: true, requireDecoupling: true) { this.beatmap = beatmap; this.skipTargetTime = skipTargetTime; From a3e4d1993331bd9dc27d482c13def9c6b7019126 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 14:32:31 +0900 Subject: [PATCH 17/62] Update tests which were not using an `IAdjustableClock` as `GameplayClockContainer` source --- osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs index 4b3163ad89..65113add2d 100644 --- a/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs +++ b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Timing; using osu.Game.Screens.Play; @@ -16,7 +17,7 @@ namespace osu.Game.Tests.NonVisual [TestCase(1)] public void TestTrueGameplayRateWithGameplayAdjustment(double underlyingClockRate) { - var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate }); + var framedClock = new TrackVirtual(60000) { Frequency = { Value = underlyingClockRate } }; var gameplayClock = new TestGameplayClockContainer(framedClock); Assert.That(gameplayClock.GetTrueGameplayRate(), Is.EqualTo(2)); @@ -24,7 +25,7 @@ namespace osu.Game.Tests.NonVisual private partial class TestGameplayClockContainer : GameplayClockContainer { - public TestGameplayClockContainer(IFrameBasedClock underlyingClock) + public TestGameplayClockContainer(IClock underlyingClock) : base(underlyingClock, false, false) { AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(2.0)); From c40bd741383107ec806680f4346468e224ea3dc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 14:40:10 +0900 Subject: [PATCH 18/62] Update usages of `GameplayClockContainer` not using an adjustable source --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 4 ++-- .../Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs | 4 ++-- .../Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 22600172dd..f8226eb21d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -6,11 +6,11 @@ using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Mods; @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock(), false, false); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false); // best way to check without exposing. private Drawable hideTarget => hudOverlay.ChildrenOfType().First(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index f1e9c831a6..656873e9ed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -4,10 +4,10 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock(), false, false); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false); [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index 782cbb0a85..4cb0d5c0ff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -8,11 +8,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached(typeof(IGameplayClock))] - private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock(), false, false); + private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false); private IEnumerable hudOverlays => CreatedDrawables.OfType(); From faa0481fc6291226cf0c836f2f33c7f0f14831a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 16:11:02 +0900 Subject: [PATCH 19/62] Fix editor operating directly on track rather than decoupled clock --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 1cdca5754d..75d9820680 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -859,7 +859,7 @@ namespace osu.Game.Screens.Edit private void resetTrack(bool seekToStart = false) { - Beatmap.Value.Track.Stop(); + clock.Stop(); if (seekToStart) { From 251a85db435b4eb7958f4fb7dc444639b580b977 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 16:18:43 +0900 Subject: [PATCH 20/62] Fix `StopUsingBeatmapClock` not transferring time and running state --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 9ecabd9c6b..70d9ecd3e7 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -187,7 +187,13 @@ namespace osu.Game.Screens.Play public void StopUsingBeatmapClock() { removeSourceClockAdjustments(); - ChangeSource(new TrackVirtual(beatmap.Track.Length)); + + var virtualTrack = new TrackVirtual(beatmap.Track.Length); + virtualTrack.Seek(CurrentTime); + if (IsRunning) + virtualTrack.Start(); + ChangeSource(virtualTrack); + addSourceClockAdjustments(); } From d019cb5167acdd6e77190a11993c4e3ddf0a1b02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Sep 2023 22:02:18 +0900 Subject: [PATCH 21/62] Update in line with framed clock changes --- osu.Game/Beatmaps/FramedBeatmapClock.cs | 7 ++----- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/FramedBeatmapClock.cs b/osu.Game/Beatmaps/FramedBeatmapClock.cs index 5fdabaab46..316d1f8a34 100644 --- a/osu.Game/Beatmaps/FramedBeatmapClock.cs +++ b/osu.Game/Beatmaps/FramedBeatmapClock.cs @@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps private IDisposable? beatmapOffsetSubscription; - private readonly DecouplingClock decoupledTrack; + private readonly DecouplingFramedClock decoupledTrack; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps { this.applyOffsets = applyOffsets; - decoupledTrack = new DecouplingClock(source) { AllowDecoupling = requireDecoupling }; + decoupledTrack = new DecouplingFramedClock(source) { AllowDecoupling = requireDecoupling }; // An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting // high precision times (on windows there's generally only 5-10ms reporting intervals, as an example). @@ -110,9 +110,6 @@ namespace osu.Game.Beatmaps { base.Update(); - if (decoupledTrack.Source is IFrameBasedClock framedClock) - framedClock.ProcessFrame(); - finalClockSource.ProcessFrame(); if (Clock.ElapsedFrameTime != 0) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 808680b9e5..aab3afcd24 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Menu { PrepareMenuLoad(); - var decouplingClock = new DecouplingClock(UsingThemedIntro ? Track : null); + var decouplingClock = new DecouplingFramedClock(UsingThemedIntro ? Track : null); LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground()) { From 81485c548c84226d4e6f59e9b7f221b376eb449d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 14:19:26 +0900 Subject: [PATCH 22/62] Move `LegacyLastTickOffset` specification to generation code and stop passing everywhere --- .../Beatmaps/CatchBeatmapConverter.cs | 1 - .../Objects/JuiceStream.cs | 4 +--- .../Editor/TestSceneSliderSplitting.cs | 1 - .../Beatmaps/OsuBeatmapConverter.cs | 1 - .../Sliders/SliderSelectionBlueprint.cs | 1 - .../Mods/OsuModStrictTracking.cs | 5 ++--- osu.Game.Rulesets.Osu/Objects/Slider.cs | 6 ++--- .../Objects/SliderTailCircle.cs | 2 +- .../Beatmaps/SliderEventGenerationTest.cs | 16 +++++++------- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 4 +--- .../Rulesets/Objects/SliderEventGenerator.cs | 22 ++++++++++++++----- .../Objects/Types/IHasLegacyLastTickOffset.cs | 14 ------------ 12 files changed, 32 insertions(+), 45 deletions(-) delete mode 100644 osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 5e8a0b1216..6a24c26844 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps X = xPositionData?.X ?? 0, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, - LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0, LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y, SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1 }.Yield(); diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 724bdc3401..87d316d36d 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Objects int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; - foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken)) { // generate tiny droplets since the last point if (lastEvent != null) @@ -162,7 +162,5 @@ namespace osu.Game.Rulesets.Catch.Objects public double Distance => Path.Distance; public IList> NodeSamples { get; set; } = new List>(); - - public double? LegacyLastTickOffset { get; set; } } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 8ba97892fe..7315344295 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -163,7 +163,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor slider = new Slider { Position = new Vector2(0, 50), - LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index f3aaf831d3..3c051a6bb1 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Osu.Beatmaps Position = positionData?.Position ?? Vector2.Zero, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, - LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset, // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // this results in more (or less) ticks being generated in s.With()).ToList(), RepeatCount = HitObject.RepeatCount, NodeSamples = HitObject.NodeSamples.Select(n => (IList)n.Select(s => s.With()).ToList()).ToList(), diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 9b5d405025..78062a0632 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -96,14 +96,13 @@ namespace osu.Game.Rulesets.Osu.Mods Position = original.Position; NewCombo = original.NewCombo; ComboOffset = original.ComboOffset; - LegacyLastTickOffset = original.LegacyLastTickOffset; TickDistanceMultiplier = original.TickDistanceMultiplier; SliderVelocityMultiplier = original.SliderVelocityMultiplier; } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken); foreach (var e in sliderEvents) { @@ -130,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Mods }); break; - case SliderEventType.LegacyLastTick: + case SliderEventType.LastTick: AddNested(TailCircle = new StrictTrackingSliderTailCircle(this) { RepeatIndex = e.SpanIndex, diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e05dbd8ea6..9736c69e17 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -71,8 +71,6 @@ namespace osu.Game.Rulesets.Osu.Objects } } - public double? LegacyLastTickOffset { get; set; } - /// /// The position of the cursor at the point of completion of this if it was hit /// with as few movements as possible. This is set and used by difficulty calculation. @@ -179,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects { base.CreateNestedHitObjects(cancellationToken); - var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken); + var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken); foreach (var e in sliderEvents) { @@ -206,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects }); break; - case SliderEventType.LegacyLastTick: + case SliderEventType.LastTick: // we need to use the LegacyLastTick here for compatibility reasons (difficulty). // it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay. // if this is to change, we should revisit this. diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index b4574791d2..fb81936837 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Objects { /// /// Note that this should not be used for timing correctness. - /// See usage in for more information. + /// See usage in for more information. /// public class SliderTailCircle : SliderEndCircle { diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index a26c8121dd..37a91c8611 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSingleSpan() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestRepeat() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestNonEvenTicks() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -83,12 +83,12 @@ namespace osu.Game.Tests.Beatmaps } [Test] - public void TestLegacyLastTickOffset() + public void TestLastTickOffset() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray(); - Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); - Assert.That(events[2].Time, Is.EqualTo(900)); + Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LastTick)); + Assert.That(events[2].Time, Is.EqualTo(span_duration + SliderEventGenerator.LAST_TICK_OFFSET)); } [Test] @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps const double velocity = 5; const double min_distance = velocity * 10; - var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2).ToArray(); Assert.Multiple(() => { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index d91ecf956a..ab8fd2c662 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -13,7 +13,7 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, IHasSliderVelocity + internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity { /// /// Scoring distance with a speed-adjusted beat length of 1 second. @@ -59,7 +59,5 @@ namespace osu.Game.Rulesets.Objects.Legacy Velocity = scoringDistance / timingPoint.BeatLength; } - - public double LegacyLastTickOffset => 36; } } diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 7013d32cbc..b3477a5fde 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -10,9 +10,17 @@ namespace osu.Game.Rulesets.Objects { public static class SliderEventGenerator { - // ReSharper disable once MethodOverloadWithOptionalParameter + /// + /// Historically, slider's final tick (aka the place where the slider would receive a final judgement) was offset by -36 ms. Originally this was + /// done to workaround a technical detail (unimportant), but over the years it has become an expectation of players that you don't need to hold + /// until the true end of the slider. This very small amount of leniency makes it easier to jump away from fast sliders to the next hit object. + /// + /// After discussion on how this should be handled going forward, players have unanimously stated that this lenience should remain in some way. + /// + public const double LAST_TICK_OFFSET = -36; + public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, - double? legacyLastTickOffset, CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default) { // A very lenient maximum length of a slider for ticks to be generated. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. @@ -76,14 +84,14 @@ namespace osu.Game.Rulesets.Objects int finalSpanIndex = spanCount - 1; double finalSpanStartTime = startTime + finalSpanIndex * spanDuration; - double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0)); + double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) + LAST_TICK_OFFSET); double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration; if (spanCount % 2 == 0) finalProgress = 1 - finalProgress; yield return new SliderEventDescriptor { - Type = SliderEventType.LegacyLastTick, + Type = SliderEventType.LastTick, SpanIndex = finalSpanIndex, SpanStartTime = finalSpanStartTime, Time = finalSpanEndTime, @@ -173,7 +181,11 @@ namespace osu.Game.Rulesets.Objects public enum SliderEventType { Tick, - LegacyLastTick, + + /// + /// Occurs just before the tail. See . + /// + LastTick, Head, Tail, Repeat diff --git a/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs b/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs deleted file mode 100644 index caf22c3023..0000000000 --- a/osu.Game/Rulesets/Objects/Types/IHasLegacyLastTickOffset.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Objects.Types -{ - /// - /// A type of which may require the last tick to be offset. - /// This is specific to osu!stable conversion, and should not be used elsewhere. - /// - public interface IHasLegacyLastTickOffset - { - double LegacyLastTickOffset { get; } - } -} From d7119674e8b5063d5350fbed78046aa873865a45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 14:40:44 +0900 Subject: [PATCH 23/62] Update comments to better explain what `LastTick` is doing --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs | 2 +- .../Objects/Drawables/DrawableSlider.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 11 +++++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 87d316d36d..fb1a86d8c0 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -104,8 +104,8 @@ namespace osu.Game.Rulesets.Catch.Objects } } - // this also includes LegacyLastTick and this is used for TinyDroplet generation above. - // this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied. + // this also includes LastTick and this is used for TinyDroplet generation above. + // this means that the final segment of TinyDroplets are increasingly mistimed where LastTick is being applied. lastEvent = e; switch (e.Type) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs index 9537f8b388..e1123807cd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSynesthesia.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Mods d.HitObjectApplied += _ => { - // slider tails are a painful edge case, as their start time is offset 36ms back (see `LegacyLastTick`). + // slider tails are a painful edge case, as their start time is offset 36ms back (see `LastTick`). // to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap. double snapTime = d is DrawableSliderTail tail ? tail.Slider.GetEndTime() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 1a6a0a9ecc..77e60a1690 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override void PlaySamples() { // rather than doing it this way, we should probably attach the sample to the tail circle. - // this can only be done after we stop using LegacyLastTick. + // this can only be done if we stop using LastTick. if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit) base.PlaySamples(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 9736c69e17..443e4229d2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -205,9 +205,10 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.LastTick: - // we need to use the LegacyLastTick here for compatibility reasons (difficulty). - // it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay. - // if this is to change, we should revisit this. + // Of note, we are directly mapping LastTick (instead of `SliderEventType.Tail`) to SliderTailCircle. + // It is required as difficulty calculation and gameplay relies on reading this value. + // (although it is displayed in classic skins, which may be a concern). + // If this is to change, we should revisit this. AddNested(TailCircle = new SliderTailCircle(this) { RepeatIndex = e.SpanIndex, @@ -262,7 +263,9 @@ namespace osu.Game.Rulesets.Osu.Objects if (HeadCircle != null) HeadCircle.Samples = this.GetNodeSamples(0); - // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. + // The samples should be attached to the slider tail, however this can only be done if LastTick is removed otherwise they would play earlier than they're intended to. + // (see mapping logic in `CreateNestedHitObjects` above) + // // For now, the samples are played by the slider itself at the correct end time. TailSamples = this.GetNodeSamples(repeatCount + 1); } From 0bcb99f2c46cf08e0c05e1cd27a0662a4a1e375b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Sep 2023 01:16:56 +0300 Subject: [PATCH 24/62] Crop oversized gameplay textures instead of downscaling them --- osu.Game/Skinning/LegacySkinExtensions.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index dde6c1fa29..868f36fb34 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osuTK; @@ -112,9 +113,11 @@ namespace osu.Game.Skinning if (texture.DisplayWidth <= maxSize.X && texture.DisplayHeight <= maxSize.Y) return texture; - // use scale adjust property for downscaling the texture in order to meet the specified maximum dimensions. - texture.ScaleAdjust *= Math.Max(texture.DisplayWidth / maxSize.X, texture.DisplayHeight / maxSize.Y); - return texture; + maxSize *= texture.ScaleAdjust; + + var croppedTexture = texture.Crop(new RectangleF(texture.Width / 2f - maxSize.X / 2f, texture.Height / 2f - maxSize.Y / 2f, maxSize.X, maxSize.Y)); + croppedTexture.ScaleAdjust = texture.ScaleAdjust; + return croppedTexture; } public static bool HasFont(this ISkin source, LegacyFont font) From 314ecec65b34c73a293e7f8fbf14183c5dcfa6b4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 30 Sep 2023 01:17:59 +0300 Subject: [PATCH 25/62] Refactor player max dimensions test scene to actually upscale textures --- .../Gameplay/TestScenePlayerMaxDimensions.cs | 77 ++++++++++++++++--- osu.Game/Skinning/Skin.cs | 5 +- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs index a8ed44c7f8..68443b234b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -3,14 +3,20 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Skinning; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; namespace osu.Game.Tests.Visual.Gameplay { @@ -23,6 +29,9 @@ namespace osu.Game.Tests.Visual.Gameplay /// public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers { + // scale textures to 4 times their size. + private const int scale_factor = 4; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -63,18 +72,66 @@ namespace osu.Game.Tests.Visual.Gameplay remove { } } - public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) - { - var texture = base.GetTexture(componentName, wrapModeS, wrapModeT); - - if (texture != null) - texture.ScaleAdjust /= 8f; - - return texture; - } - public ISkin FindProvider(Func lookupFunction) => this; public IEnumerable AllSources => new[] { this }; + + protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) + => new UpscaledTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage)); + + private class UpscaledTextureLoaderStore : IResourceStore + { + private readonly IResourceStore? textureStore; + + public UpscaledTextureLoaderStore(IResourceStore? textureStore) + { + this.textureStore = textureStore; + } + + public void Dispose() + { + textureStore?.Dispose(); + } + + public TextureUpload Get(string name) + { + var textureUpload = textureStore?.Get(name); + + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureUpload == null) + return null!; + + return upscale(textureUpload); + } + + public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureStore == null) + return null!; + + var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); + + if (textureUpload == null) + return null!; + + return await Task.Run(() => upscale(textureUpload), cancellationToken).ConfigureAwait(false); + } + + private TextureUpload upscale(TextureUpload textureUpload) + { + var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + + // The original texture upload will no longer be returned or used. + textureUpload.Dispose(); + + image.Mutate(i => i.Resize(new Size(textureUpload.Width, textureUpload.Height) * scale_factor)); + return new TextureUpload(image); + } + + public Stream? GetStream(string name) => textureStore?.GetStream(name); + + public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty(); + } } } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index ccf49d722f..1e312142d7 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -88,7 +88,7 @@ namespace osu.Game.Skinning } Samples = samples; - Textures = new TextureStore(resources.Renderer, new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage))); + Textures = new TextureStore(resources.Renderer, CreateTextureLoaderStore(resources, storage)); } else { @@ -171,6 +171,9 @@ namespace osu.Game.Skinning } } + protected virtual IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) + => new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage)); + protected virtual void ParseConfigurationStream(Stream stream) { using (LineBufferedReader reader = new LineBufferedReader(stream, true)) From 622cbc3af7e787257f8d5f31f0c98bfef8a97cf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 16:07:45 +0900 Subject: [PATCH 26/62] Adjust test scene to actually make HP bars fit --- .../Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index 4d8ddcd581..8d3eee2445 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Tests.Visual.Gameplay { @@ -20,9 +21,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); - protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay(); - protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay(); - protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay(); + protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f) }; + protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) }; + protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) }; [SetUpSteps] public void SetUpSteps() @@ -62,4 +63,4 @@ namespace osu.Game.Tests.Visual.Gameplay }, 3); } } -} \ No newline at end of file +} From 82ba545358723a8560de4444568b107f029a6bfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 17:01:56 +0900 Subject: [PATCH 27/62] Add initial animation for health bars --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 4 +- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 42 ++++++++++++++++++- osu.Game/Skinning/LegacyHealthDisplay.cs | 1 + 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 6cf1daa102..68685d7eb5 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Play.HUD } } - private double missBarValue = 1.0; + private double missBarValue; private readonly List missBarVertices = new List(); public double MissBarValue @@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play.HUD } } - private double healthBarValue = 1.0; + private double healthBarValue; private readonly List healthBarVertices = new List(); public double HealthBarValue diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 5131f93ca2..e4bb91e7ca 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -6,7 +6,9 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -23,12 +25,16 @@ namespace osu.Game.Screens.Play.HUD [Resolved] protected HealthProcessor HealthProcessor { get; private set; } = null!; - public Bindable Current { get; } = new BindableDouble(1) + public Bindable Current { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; + private BindableNumber health = null!; + + private ScheduledDelegate? initialIncrease; + /// /// Triggered when a is a successful hit, signaling the health display to perform a flash animation (if designed to do so). /// @@ -52,14 +58,46 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - Current.BindTo(HealthProcessor.Health); HealthProcessor.NewJudgement += onNewJudgement; + // Don't bind directly so we can animate the startup procedure. + health = HealthProcessor.Health.GetBoundCopy(); + health.BindValueChanged(h => + { + Current.Value = h.NewValue; + finishInitialAnimation(); + }); + if (hudOverlay != null) showHealthBar.BindTo(hudOverlay.ShowHealthBar); // this probably shouldn't be operating on `this.` showHealthBar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true); + + startInitialAnimation(); + } + + private void startInitialAnimation() + { + // TODO: this should run in gameplay time, including showing a larger increase when skipping. + // TODO: it should also start increasing relative to the first hitobject. + const double increase_delay = 150; + + initialIncrease = Scheduler.AddDelayed(() => + { + double newValue = Current.Value + 0.05f; + this.TransformBindableTo(Current, newValue, increase_delay); + Flash(new JudgementResult(new HitObject(), new Judgement())); + + if (newValue >= 1) + finishInitialAnimation(); + }, increase_delay, true); + } + + private void finishInitialAnimation() + { + initialIncrease?.Cancel(); + initialIncrease = null; } private void onNewJudgement(JudgementResult judgement) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index f785022f84..08add79fc1 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -66,6 +66,7 @@ namespace osu.Game.Skinning marker.Current.BindTo(Current); maxFillWidth = fill.Width; + fill.Width = 0; } protected override void Update() From 48209872bfd73d347a7fa6f35958d77a659a6fbc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 4 Oct 2023 13:53:33 +0900 Subject: [PATCH 28/62] Fix spinner requirements being susceptible to FP precision --- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index aca55c6bd9..e3dfe8e69a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -70,8 +70,11 @@ namespace osu.Game.Rulesets.Osu.Objects double secondsDuration = Duration / 1000; - SpinsRequired = (int)(minRps * secondsDuration); - MaximumBonusSpins = Math.Max(0, (int)(maxRps * secondsDuration) - SpinsRequired - bonus_spins_gap); + // Allow a 0.1ms floating point precision error in the calculation of the duration. + const double duration_error = 0.0001; + + SpinsRequired = (int)(minRps * secondsDuration + duration_error); + MaximumBonusSpins = Math.Max(0, (int)(maxRps * secondsDuration + duration_error) - SpinsRequired - bonus_spins_gap); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From a186d97edf53f2152bd52b852421a789c58cfabf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Oct 2023 16:57:54 +0900 Subject: [PATCH 29/62] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 20b0f220a3..85d5e66d44 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + System.Net.Sockets.SocketException (11001): No such host is known. 2023-10-06 03:24:17 [verbose]: at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken) ``` Closes https://github.com/ppy/osu/issues/24890 (again). --- osu.Game/Online/API/OAuth.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 1f26ab5458..485274f349 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Net.Http; +using System.Net.Sockets; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -99,6 +100,11 @@ namespace osu.Game.Online.API return true; } } + catch (SocketException) + { + // Network failure. + return false; + } catch (HttpRequestException) { // Network failure. @@ -106,7 +112,7 @@ namespace osu.Game.Online.API } catch { - // Force a full re-reauthentication. + // Force a full re-authentication. Token.Value = null; return false; } From db5178e45306ce9df560b047a563b3e075a75ff6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 16:52:00 +0900 Subject: [PATCH 50/62] Change `ArgonHealthDisplay` to be relative sized for now --- .../Gameplay/TestSceneArgonHealthDisplay.cs | 2 - .../Screens/Play/HUD/ArgonHealthDisplay.cs | 68 ++++++++++++++----- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index 06a7763711..8261a1729e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; -using osuTK; using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay @@ -41,7 +40,6 @@ namespace osu.Game.Tests.Visual.Gameplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = new Vector2(2f), }, }; }); diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 62a4b958c2..ad4b407692 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -5,14 +5,17 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -27,6 +30,23 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } + [SettingSource("Bar height")] + public BindableFloat BarHeight { get; } = new BindableFloat + { + Default = 32, + MinValue = 0, + MaxValue = 64, + Precision = 1 + }; + + [SettingSource("Bar length")] + public BindableFloat BarLength { get; } = new BindableFloat(1) + { + MinValue = 0.2f, + MaxValue = 1, + Precision = 0.01f, + }; + private BarPath mainBar = null!; /// @@ -76,10 +96,13 @@ namespace osu.Game.Screens.Play.HUD } } + private const float left_line_width = 50f; + [BackgroundDependencyLoader] private void load() { - AutoSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; InternalChild = new FillFlowContainer { @@ -91,7 +114,7 @@ namespace osu.Game.Screens.Play.HUD new Circle { Margin = new MarginPadding { Top = 8.5f, Left = -2 }, - Size = new Vector2(50f, 3f), + Size = new Vector2(left_line_width, 3f), }, new Container { @@ -127,8 +150,6 @@ namespace osu.Game.Screens.Play.HUD } }, }; - - updatePath(); } protected override void LoadComplete() @@ -144,6 +165,18 @@ namespace osu.Game.Screens.Play.HUD if (resetMissBarDelegate == null) this.TransformTo(nameof(GlowBarValue), v.NewValue, 300, Easing.OutQuint); }, true); + + BarLength.BindValueChanged(l => Width = l.NewValue, true); + BarHeight.BindValueChanged(_ => updatePath()); + updatePath(); + } + + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + if ((invalidation & Invalidation.DrawSize) > 0) + updatePath(); + + return base.OnInvalidate(invalidation, source); } protected override void Update() @@ -214,25 +247,24 @@ namespace osu.Game.Screens.Play.HUD private void updatePath() { - const float curve_start = 280; - const float curve_end = 310; + float barLength = DrawWidth - left_line_width - 24; + float curveStart = barLength - 70; + float curveEnd = barLength - 40; + const float curve_smoothness = 10; - const float bar_length = 350; - const float bar_verticality = 32.5f; - - Vector2 diagonalDir = (new Vector2(curve_end, bar_verticality) - new Vector2(curve_start, 0)).Normalized(); + Vector2 diagonalDir = (new Vector2(curveEnd, BarHeight.Value) - new Vector2(curveStart, 0)).Normalized(); barPath = new SliderPath(new[] { new PathControlPoint(new Vector2(0, 0), PathType.Linear), - new PathControlPoint(new Vector2(curve_start - curve_smoothness, 0), PathType.Bezier), - new PathControlPoint(new Vector2(curve_start, 0)), - new PathControlPoint(new Vector2(curve_start, 0) + diagonalDir * curve_smoothness, PathType.Linear), - new PathControlPoint(new Vector2(curve_end, bar_verticality) - diagonalDir * curve_smoothness, PathType.Bezier), - new PathControlPoint(new Vector2(curve_end, bar_verticality)), - new PathControlPoint(new Vector2(curve_end + curve_smoothness, bar_verticality), PathType.Linear), - new PathControlPoint(new Vector2(bar_length, bar_verticality)), + new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.Bezier), + new PathControlPoint(new Vector2(curveStart, 0)), + new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.Linear), + new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.Bezier), + new PathControlPoint(new Vector2(curveEnd, BarHeight.Value)), + new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.Linear), + new PathControlPoint(new Vector2(barLength, BarHeight.Value)), }); List vertices = new List(); @@ -267,7 +299,7 @@ namespace osu.Game.Screens.Play.HUD { protected override Color4 ColourAt(float position) { - if (position <= 0.128f) + if (position <= 0.16f) return Color4.White.Opacity(0.8f); return Interpolation.ValueAt(position, From f40e910c51da3f4ad5779b854941ffb5b8e53a23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 18:56:31 +0900 Subject: [PATCH 51/62] Remove left line from health display --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 67 +++++++------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index ad4b407692..67f21a1c83 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; @@ -96,7 +95,7 @@ namespace osu.Game.Screens.Play.HUD } } - private const float left_line_width = 50f; + private const float main_path_radius = 10f; [BackgroundDependencyLoader] private void load() @@ -104,51 +103,37 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new FillFlowContainer + InternalChild = new Container { AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4f, 0f), Children = new Drawable[] { - new Circle + background = new BackgroundPath { - Margin = new MarginPadding { Top = 8.5f, Left = -2 }, - Size = new Vector2(left_line_width, 3f), + PathRadius = main_path_radius, }, - new Container + glowBar = new BarPath { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - background = new BackgroundPath - { - PathRadius = 10f, - }, - glowBar = new BarPath - { - BarColour = Color4.White, - GlowColour = OsuColour.Gray(0.5f), - Blending = BlendingParameters.Additive, - Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), - PathRadius = 40f, - // Kinda hacky, but results in correct positioning with increased path radius. - Margin = new MarginPadding(-30f), - GlowPortion = 0.9f, - }, - mainBar = new BarPath - { - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - BarColour = main_bar_colour, - GlowColour = main_bar_glow_colour, - PathRadius = 10f, - GlowPortion = 0.6f, - }, - } - } - }, + BarColour = Color4.White, + GlowColour = OsuColour.Gray(0.5f), + Blending = BlendingParameters.Additive, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), + PathRadius = 40f, + // Kinda hacky, but results in correct positioning with increased path radius. + Margin = new MarginPadding(-30f), + GlowPortion = 0.9f, + }, + mainBar = new BarPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + BarColour = main_bar_colour, + GlowColour = main_bar_glow_colour, + PathRadius = main_path_radius, + GlowPortion = 0.6f, + }, + } }; } @@ -247,7 +232,7 @@ namespace osu.Game.Screens.Play.HUD private void updatePath() { - float barLength = DrawWidth - left_line_width - 24; + float barLength = DrawWidth - main_path_radius * 2; float curveStart = barLength - 70; float curveEnd = barLength - 40; From 71be3c8f8b783716d4bcb619d36e253b1ad79315 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 18:56:16 +0900 Subject: [PATCH 52/62] Add ability to adjust health bar settings in test scene --- .../Gameplay/TestSceneArgonHealthDisplay.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index 8261a1729e..7bad623d7f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; @@ -21,6 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); + private ArgonHealthDisplay healthDisplay = null!; + [SetUpSteps] public void SetUpSteps() { @@ -36,13 +39,25 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, Colour = Color4.Gray, }, - new ArgonHealthDisplay + healthDisplay = new ArgonHealthDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, }, }; }); + + AddSliderStep("Width", 0, 1f, 1f, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.BarLength.Value = val; + }); + + AddSliderStep("Height", 0, 64, 0, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.BarHeight.Value = val; + }); } [Test] From 3f2a00d90d2a967147e33e273dab24779fb03747 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 18:46:50 +0900 Subject: [PATCH 53/62] Add argon health display to default skin layout --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 5 ++--- osu.Game/Skinning/ArgonSkin.cs | 10 +++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 67f21a1c83..755eaeaf33 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -30,16 +30,15 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } [SettingSource("Bar height")] - public BindableFloat BarHeight { get; } = new BindableFloat + public BindableFloat BarHeight { get; } = new BindableFloat(20) { - Default = 32, MinValue = 0, MaxValue = 64, Precision = 1 }; [SettingSource("Bar length")] - public BindableFloat BarLength { get; } = new BindableFloat(1) + public BindableFloat BarLength { get; } = new BindableFloat(0.98f) { MinValue = 0.2f, MaxValue = 1, diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 6e17458082..d530efbfdd 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -109,6 +109,7 @@ namespace osu.Game.Skinning case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { + var health = container.OfType().FirstOrDefault(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); @@ -128,6 +129,13 @@ namespace osu.Game.Skinning score.Position = new Vector2(0, vertical_offset); + if (health != null) + { + health.Origin = Anchor.TopCentre; + health.Anchor = Anchor.TopCentre; + health.Y = 5; + } + if (ppCounter != null) { ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4; @@ -191,7 +199,7 @@ namespace osu.Game.Skinning new DefaultComboCounter(), new DefaultScoreCounter(), new DefaultAccuracyCounter(), - new DefaultHealthDisplay(), + new ArgonHealthDisplay(), new ArgonSongProgress(), new ArgonKeyCounterDisplay(), new BarHitErrorMeter(), From d87ab9c82dad1081c6a060ea8b6e401bbd29cdee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 19:34:38 +0900 Subject: [PATCH 54/62] Adjust transition time based on miss/hit --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 755eaeaf33..7af7fd9487 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -145,9 +145,11 @@ namespace osu.Game.Screens.Play.HUD if (v.NewValue >= GlowBarValue) finishMissDisplay(); - this.TransformTo(nameof(HealthBarValue), v.NewValue, 300, Easing.OutQuint); + double time = v.NewValue > GlowBarValue ? 500 : 250; + + this.TransformTo(nameof(HealthBarValue), v.NewValue, time, Easing.OutQuint); if (resetMissBarDelegate == null) - this.TransformTo(nameof(GlowBarValue), v.NewValue, 300, Easing.OutQuint); + this.TransformTo(nameof(GlowBarValue), v.NewValue, time, Easing.OutQuint); }, true); BarLength.BindValueChanged(l => Width = l.NewValue, true); From 8e5b2e78e58842721478fbf5dd7fdd7c7a1f66ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Oct 2023 21:01:23 +0900 Subject: [PATCH 55/62] Fix variable clash --- osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index 54cd36d05b..dd6536cf26 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -117,17 +117,17 @@ namespace osu.Game.Screens.OnlinePlay private void updateModDisplay() { - int current = Current.Value.Count; + int currentCount = Current.Value.Count; - if (current == allAvailableAndValidMods.Count()) + if (currentCount == allAvailableAndValidMods.Count()) { count.Text = "all"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.Yellow, 200, Easing.OutQuint); } - else if (current > 0) + else if (currentCount > 0) { - count.Text = $"{current} mods"; + count.Text = $"{currentCount} mods"; count.FadeColour(colours.Gray2, 200, Easing.OutQuint); circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint); } From 10ce5705ce67acb920f2f4f0d6b30e91178df5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Oct 2023 14:11:41 +0200 Subject: [PATCH 56/62] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index bc95e96a7b..b3feccbbc0 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - +