From 32c60bfb36ae428e6fe56b077d9397c6bc57dd30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Apr 2025 19:31:16 +0900 Subject: [PATCH] Disallow adjusting scroll speed during gameplay Matches stable. Addresses https://github.com/ppy/osu/discussions/32670. --- .../UI/DrawableManiaRuleset.cs | 11 +++- .../Navigation/TestSceneScreenNavigation.cs | 58 +++++++++++++++++++ .../UI/Scrolling/DrawableScrollingRuleset.cs | 30 ++++++---- osu.Game/Screens/Play/Player.cs | 6 ++ .../PlayerSettings/BeatmapOffsetControl.cs | 25 +------- osu.Game/Screens/Play/SubmittingPlayer.cs | 18 ++++++ 6 files changed, 112 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 66400b0a55..fe3535d857 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -60,8 +60,9 @@ namespace osu.Game.Rulesets.Mania.UI private readonly BindableDouble configScrollSpeed = new BindableDouble(); private readonly Bindable mobileLayout = new Bindable(); + public double TargetTimeRange { get; protected set; } + private double currentTimeRange; - protected double TargetTimeRange; // Stores the current speed adjustment active in gameplay. private readonly Track speedAdjustmentTrack = new TrackVirtual(0); @@ -109,7 +110,13 @@ namespace osu.Game.Rulesets.Mania.UI configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed); - configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); + configScrollSpeed.BindValueChanged(speed => + { + if (!AllowScrollSpeedAdjustment) + return; + + TargetTimeRange = ComputeScrollTime(speed.NewValue); + }); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8c4fcc461c..312781ef1a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -33,6 +33,10 @@ using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Toolbar; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; @@ -394,6 +398,60 @@ namespace osu.Game.Tests.Visual.Navigation } } + [Test] + public void TestScrollSpeedAdjustDuringGameplay() + { + Player player = null; + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("switch to mania ruleset", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.Number4); + InputManager.ReleaseKey(Key.LControl); + }); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() }); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + player = Game.ScreenStack.CurrentScreen as Player; + return player?.IsLoaded == true; + }); + + AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); + checkScrollSpeed(8, 8); + + AddStep("adjust scroll speed via keyboard", () => InputManager.Key(Key.F4)); + checkScrollSpeed(9, 9); + + AddStep("seek beyond 10 seconds", () => player.ChildrenOfType().First().Seek(10500)); + AddUntilStep("wait for seek", () => player.ChildrenOfType().First().CurrentTime, () => Is.GreaterThan(10600)); + AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.F4)); + checkScrollSpeed(9, 9); + + AddStep("attempt adjust offset via config change", () => getConfigManager().SetValue(ManiaRulesetSetting.ScrollSpeed, 10.0)); + checkScrollSpeed(10, 9); + + void checkScrollSpeed(double configValue, double gameplayValue) + { + AddUntilStep($"config value is {configValue}", () => getConfigManager().Get(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue)); + AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType().Single().TargetTimeRange, + () => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue))); + } + + ManiaRulesetConfigManager getConfigManager() => ((ManiaRulesetConfigManager)Game.Dependencies.Get().GetConfigFor(new ManiaRuleset())!); + } + [Test] public void TestOffsetAdjustDuringGameplay() { diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index ba3a9bd483..f0b9876b51 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -21,6 +19,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI.Scrolling.Algorithms; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.UI.Scrolling { @@ -69,6 +68,12 @@ namespace osu.Game.Rulesets.UI.Scrolling /// protected virtual bool UserScrollSpeedAdjustment => true; + /// + /// Whether at the current point in time, whether scroll speed adjustments should be applied to gameplay. + /// This can potentially become false at some point during gameplay for game balance reasons. + /// + protected bool AllowScrollSpeedAdjustment => UserScrollSpeedAdjustment && player?.AllowCriticalSettingsAdjustment != false; + /// /// Whether beat lengths should scale relative to the most common beat length in the . /// @@ -84,7 +89,10 @@ namespace osu.Game.Rulesets.UI.Scrolling [Cached(Type = typeof(IScrollingInfo))] private readonly LocalScrollingInfo scrollingInfo; - protected DrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + [Resolved] + private Player? player { get; set; } + + protected DrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) : base(ruleset, beatmap, mods) { scrollingInfo = new LocalScrollingInfo(); @@ -195,28 +203,30 @@ namespace osu.Game.Rulesets.UI.Scrolling /// Adjusts the scroll speed of s. /// /// The amount to adjust by. Greater than 0 if the scroll speed should be increased, less than 0 if it should be decreased. - protected virtual void AdjustScrollSpeed(int amount) => this.TransformBindableTo(TimeRange, TimeRange.Value - amount * time_span_step, 200, Easing.OutQuint); + protected virtual void AdjustScrollSpeed(int amount) + { + this.TransformBindableTo(TimeRange, TimeRange.Value - amount * time_span_step, 200, Easing.OutQuint); + } public bool OnPressed(KeyBindingPressEvent e) { - if (!UserScrollSpeedAdjustment) - return false; - switch (e.Action) { case GlobalAction.IncreaseScrollSpeed: - AdjustScrollSpeed(1); + if (AllowScrollSpeedAdjustment) + AdjustScrollSpeed(1); return true; case GlobalAction.DecreaseScrollSpeed: - AdjustScrollSpeed(-1); + if (AllowScrollSpeedAdjustment) + AdjustScrollSpeed(-1); return true; } return false; } - private ScheduledDelegate scheduledScrollSpeedAdjustment; + private ScheduledDelegate? scheduledScrollSpeedAdjustment; public void OnReleased(KeyBindingReleaseEvent e) { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 612d66a896..b2e502406a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -112,6 +112,12 @@ namespace osu.Game.Screens.Play /// public IBindable ShowingOverlayComponents = new Bindable(); + /// + /// A flag which can be checked to decide whether we are in a state where settings that affect + /// game balance should be allowed to be applied at the current point in time. + /// + public virtual bool AllowCriticalSettingsAdjustment { get; } = true; + // Should match PlayerLoader for consistency. Cached here for the rare case we push a Player // without the loading screen (one such usage is the skin editor's scene library). [Cached] diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 23ccb3311b..b0b4f6cc5d 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -60,9 +60,6 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private Player? player { get; set; } - [Resolved] - private IGameplayClock? gameplayClock { get; set; } - private double lastPlayMedian; private double lastPlayBeatmapOffset; private HitEventTimingDistributionGraph? lastPlayGraph; @@ -287,27 +284,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Current.Disabled = !allow; } - private bool allowOffsetAdjust - { - get - { - // General limitations to ensure players don't do anything too weird. - // These match stable for now. - if (player is SubmittingPlayer) - { - Debug.Assert(gameplayClock != null); - - // TODO: the blocking conditions should probably display a message. - if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.GameplayStartTime > 10000) - return false; - - if (gameplayClock.IsPaused.Value) - return false; - } - - return true; - } - } + private bool allowOffsetAdjust => player?.AllowCriticalSettingsAdjustment != false; public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index dc3e5f08ac..7becb2b33e 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -186,6 +186,24 @@ namespace osu.Game.Screens.Play /// Whether gameplay should be immediately exited as a result. Returning false allows the gameplay session to continue. Defaults to true. protected virtual bool ShouldExitOnTokenRetrievalFailure(Exception exception) => true; + public override bool AllowCriticalSettingsAdjustment + { + get + { + // General limitations to ensure players don't do anything too weird. + // These match stable for now. + + // TODO: the blocking conditions should probably display a message. + if (!IsBreakTime.Value && GameplayClockContainer.CurrentTime - GameplayClockContainer.GameplayStartTime > 10000) + return false; + + if (GameplayClockContainer.IsPaused.Value) + return false; + + return base.AllowCriticalSettingsAdjustment; + } + } + protected override async Task PrepareScoreForResultsAsync(Score score) { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);