diff --git a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs new file mode 100644 index 0000000000..efb65bb0a8 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings.Sections.Audio; +using osu.Game.Scoring; +using osu.Game.Tests.Visual.Ranking; + +namespace osu.Game.Tests.Visual.Settings +{ + public partial class TestSceneAudioOffsetAdjustControl : OsuTestScene + { + [Resolved] + private SessionStatics statics { get; set; } = null!; + + [Cached] + private SessionAverageHitErrorTracker tracker = new SessionAverageHitErrorTracker(); + + private Container content = null!; + protected override Container Content => content; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.AddRange(new Drawable[] + { + tracker, + content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + AutoSizeAxes = Axes.Y + } + }); + } + + [Test] + public void TestBehaviour() + { + AddStep("create control", () => Child = new AudioOffsetAdjustControl + { + Current = new BindableDouble + { + MinValue = -500, + MaxValue = 500 + } + }); + AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(RNG.NextDouble(-100, 100)), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + })); + AddStep("clear history", () => tracker.ClearHistory()); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 64f15efb15..0d8a2fbb97 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -201,6 +201,8 @@ namespace osu.Game private RulesetConfigCache rulesetConfigCache; + private SessionAverageHitErrorTracker hitErrorTracker; + protected SpectatorClient SpectatorClient { get; private set; } protected MultiplayerClient MultiplayerClient { get; private set; } @@ -350,7 +352,7 @@ namespace osu.Game dependencies.CacheAs(powerStatus); dependencies.Cache(SessionStatics = new SessionStatics()); - dependencies.Cache(new SessionAverageHitErrorTracker()); + dependencies.Cache(hitErrorTracker = new SessionAverageHitErrorTracker()); dependencies.Cache(Colours = new OsuColour()); RegisterImportHandler(BeatmapManager); @@ -410,6 +412,7 @@ namespace osu.Game }); base.Content.Add(new TouchInputInterceptor()); + base.Content.Add(hitErrorTracker); KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 045cd24fc0..0023c60c61 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -1,9 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osuTK; namespace osu.Game.Overlays.Settings.Sections.Audio { @@ -12,12 +26,134 @@ namespace osu.Game.Overlays.Settings.Sections.Audio [BackgroundDependencyLoader] private void load() { + LabelText = AudioSettingsStrings.AudioOffset; } protected override Drawable CreateControl() => new AudioOffsetPreview(); - private partial class AudioOffsetPreview : CompositeDrawable + private partial class AudioOffsetPreview : CompositeDrawable, IHasCurrentValue { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + + private readonly IBindableList averageHitErrorHistory = new BindableList(); + + private readonly Bindable suggestedOffset = new Bindable(); + + private Container notchContainer = null!; + private TextFlowContainer hintText = null!; + private RoundedButton applySuggestion = null!; + + [BackgroundDependencyLoader] + private void load(SessionAverageHitErrorTracker hitErrorTracker) + { + averageHitErrorHistory.BindTo(hitErrorTracker.AverageHitErrorHistory); + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TimeSlider + { + RelativeSizeAxes = Axes.X, + Current = { BindTarget = Current }, + KeyboardStep = 1, + }, + notchContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = 10, + Padding = new MarginPadding { Horizontal = Nub.DEFAULT_EXPANDED_SIZE / 2 }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + hintText = new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 16)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + applySuggestion = new RoundedButton + { + RelativeSizeAxes = Axes.X, + Text = "Apply suggested offset", + Action = () => + { + if (suggestedOffset.Value.HasValue) + current.Value = suggestedOffset.Value.Value; + hitErrorTracker.ClearHistory(); + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + averageHitErrorHistory.BindCollectionChanged(updateDisplay, true); + suggestedOffset.BindValueChanged(_ => updateHintText(), true); + } + + private void updateDisplay(object? _, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (double average in e.NewItems!) + { + notchContainer.ForEach(n => n.Alpha *= 0.95f); + notchContainer.Add(new Box + { + RelativeSizeAxes = Axes.Y, + Width = 2, + RelativePositionAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + X = getXPositionForAverage(average) + }); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (double average in e.OldItems!) + { + var notch = notchContainer.FirstOrDefault(n => n.X == getXPositionForAverage(average)); + Debug.Assert(notch != null); + notchContainer.Remove(notch, true); + } + + break; + + case NotifyCollectionChangedAction.Reset: + notchContainer.Clear(); + break; + } + + suggestedOffset.Value = averageHitErrorHistory.Count < 3 ? null : -averageHitErrorHistory.Average(); + } + + private float getXPositionForAverage(double average) => (float)(Math.Clamp(-average, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); + + private void updateHintText() + { + hintText.Text = suggestedOffset.Value == null + ? @"Play a few beatmaps to receive a suggested offset!" + : $@"Based on the last {averageHitErrorHistory.Count} plays, the suggested offset is {suggestedOffset.Value:N0} ms."; + applySuggestion.Enabled.Value = suggestedOffset.Value != null; + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index 6b5c769853..af15d310fc 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Audio @@ -23,11 +22,9 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { Children = new Drawable[] { - new SettingsSlider + new AudioOffsetAdjustControl { - LabelText = AudioSettingsStrings.AudioOffset, Current = config.GetBindable(OsuSetting.AudioOffset), - KeyboardStep = 1f }, new SettingsButton {