From b2e7da5aa06189bf52e0f5e38bf7cb9bd8b6a712 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 28 Jul 2022 18:37:12 +0200 Subject: [PATCH 01/38] Add basic Queue based implementation of KPS --- .../Gameplay/TestSceneKeysPerSecondCounter.cs | 10 ++ .../Screens/Play/HUD/KeysPerSecondCounter.cs | 125 ++++++++++++++++++ osu.Game/Screens/Play/KeyCounter.cs | 2 + 3 files changed, 137 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs create mode 100644 osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs new file mode 100644 index 000000000000..451e297e0523 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs @@ -0,0 +1,10 @@ +// 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.Tests.Visual.Gameplay +{ + public class TestSceneKeysPerSecondCounter + { + + } +} diff --git a/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs new file mode 100644 index 000000000000..dc9a51dbf399 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable + { + private static Queue? timestamps; + + private static event Action? onNewInput; + private readonly TimeSpan refreshSpan = TimeSpan.FromSeconds(1); + + private const float alpha_when_invalid = 0.3f; + private readonly Bindable valid = new Bindable(); + + public static void AddTimestamp() + { + timestamps?.Enqueue(DateTime.Now); + onNewInput?.Invoke(); + } + + protected override double RollingDuration => 250; + + public bool UsesFixedAnchor { get; set; } + + public KeysPerSecondCounter() + { + timestamps ??= new Queue(); + Current.Value = 0; + onNewInput += updateCounter; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.BlueLighter; + valid.BindValueChanged(e => + DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateCounter(); + } + + protected override void Update() + { + if (timestamps != null) + { + if (timestamps.TryPeek(out var earliest) && DateTime.Now - earliest >= refreshSpan) + timestamps.Dequeue(); + } + + updateCounter(); + + base.Update(); + } + + private void updateCounter() + { + valid.Value = timestamps != null; + Current.Value = timestamps?.Count ?? 0; + } + + protected override IHasText CreateText() => new TextComponent + { + Alpha = alpha_when_invalid + }; + + private class TextComponent : CompositeDrawable, IHasText + { + public LocalisableString Text + { + get => text.Text; + set => text.Text = value; + } + + private readonly OsuSpriteText text; + + public TextComponent() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(2), + Children = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Numeric.With(size: 16, fixedWidth: true) + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Numeric.With(size: 8, fixedWidth: true), + Text = @"KPS", + Padding = new MarginPadding { Bottom = 1.5f }, // align baseline better + } + } + }; + } + } + } +} diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/KeyCounter.cs index 1e5ada529570..b8bbac9a7e10 100644 --- a/osu.Game/Screens/Play/KeyCounter.cs +++ b/osu.Game/Screens/Play/KeyCounter.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Graphics; @@ -55,6 +56,7 @@ protected set public void Increment() { + KeysPerSecondCounter.AddTimestamp(); if (!IsCounting) return; From 079150849a53ee3545a8b58c137bb7648fdb4b3d Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 28 Jul 2022 18:37:50 +0200 Subject: [PATCH 02/38] Add some tests --- .../Gameplay/TestSceneKeysPerSecondCounter.cs | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs index 451e297e0523..e20a83b54a76 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs @@ -1,10 +1,75 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable disable + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osuTK; +using osuTK.Input; + namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneKeysPerSecondCounter + public class TestSceneKeysPerSecondCounter : OsuManualInputManagerTestScene { - + private KeysPerSecondCounter counter; + + [SetUpSteps] + public void Setup() + { + createCounter(); + } + + private void createCounter() => AddStep("Create counter", () => + { + Child = counter = new KeysPerSecondCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5) + }; + }); + + [Test] + public void TestManualTrigger() + { + AddAssert("Counter = 0", () => counter.Current.Value == 0); + AddRepeatStep("manual trigger", KeysPerSecondCounter.AddTimestamp, 20); + AddAssert("Counter is not 0", () => counter.Current.Value > 0); + } + + [Test] + public void TestKpsAsideKeyCounter() + { + AddStep("Create key counter display", () => + Add(new KeyCounterDisplay + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Y = 100, + Children = new KeyCounter[] + { + new KeyCounterKeyboard(Key.W), + new KeyCounterKeyboard(Key.X), + new KeyCounterKeyboard(Key.C), + new KeyCounterKeyboard(Key.V) + } + }) + ); + AddAssert("Counter = 0", () => counter.Current.Value == 0); + addPressKeyStep(Key.W); + addPressKeyStep(Key.X); + addPressKeyStep(Key.C); + addPressKeyStep(Key.V); + AddAssert("Counter = 4", () => counter.Current.Value == 4); + } + + private void addPressKeyStep(Key key) + { + AddStep($"Press {key} key", () => InputManager.Key(key)); + } } } From 89855cc1d6af208b5e132264e610b16a2386c8c4 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Sun, 31 Jul 2022 01:29:57 +0200 Subject: [PATCH 03/38] Change KPS Counter implementation base and add better replay integration The counter implementaiton is now list based, and will not invalidate previous hits by removing them but by testing if they are within the 1 second span, allowing better integration with replays and spectators. --- .../Screens/Play/HUD/KeysPerSecondCounter.cs | 67 ++++++++++++++----- osu.Game/Screens/Play/KeyCounterDisplay.cs | 2 + osu.Game/Screens/Play/Player.cs | 4 ++ 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs index dc9a51dbf399..d32ca1410acb 100644 --- a/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs @@ -1,17 +1,24 @@ // 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; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Framework.Timing; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; @@ -19,37 +26,71 @@ namespace osu.Game.Screens.Play.HUD { public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable { - private static Queue? timestamps; + private static List timestamps; + private static double maxTime = double.NegativeInfinity; - private static event Action? onNewInput; - private readonly TimeSpan refreshSpan = TimeSpan.FromSeconds(1); + private static event Action onNewInput; + private const int invalidation_timeout = 1000; private const float alpha_when_invalid = 0.3f; + private readonly Bindable valid = new Bindable(); + private static GameplayClock gameplayClock; + private static IClock referenceClock; + + private static IClock clock => referenceClock ?? gameplayClock; + + [Resolved(canBeNull: true)] + private DrawableRuleset drawableRuleset { get; set; } + + [SettingSource("Smoothing time", "How smooth the counter should change\nThe more it is smooth, the less it's accurate.")] + public BindableNumber SmoothingTime { get; } = new BindableNumber(350) + { + MaxValue = 1000, + MinValue = 0 + }; + public static void AddTimestamp() { - timestamps?.Enqueue(DateTime.Now); + Logger.Log($"Input timestamp attempt C: {clock.CurrentTime}ms | GC: {gameplayClock.CurrentTime} | RC: {referenceClock?.CurrentTime ?? -1} | Max: {maxTime})", level: LogLevel.Debug); + + if (clock.CurrentTime >= maxTime) + { + Logger.Log("Input timestamp added.", level: LogLevel.Debug); + timestamps?.Add(clock.CurrentTime); + maxTime = timestamps?.Max() ?? clock.CurrentTime; + } + onNewInput?.Invoke(); } - protected override double RollingDuration => 250; + public static void Reset() + { + timestamps?.Clear(); + maxTime = int.MinValue; + } + + protected override double RollingDuration => SmoothingTime.Value; public bool UsesFixedAnchor { get; set; } public KeysPerSecondCounter() { - timestamps ??= new Queue(); + timestamps ??= new List(); Current.Value = 0; onNewInput += updateCounter; + Scheduler.AddOnce(updateCounter); } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, GameplayClock clock) { + gameplayClock = clock; Colour = colours.BlueLighter; valid.BindValueChanged(e => DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); + referenceClock = drawableRuleset?.FrameStableClock; } protected override void LoadComplete() @@ -61,21 +102,15 @@ protected override void LoadComplete() protected override void Update() { - if (timestamps != null) - { - if (timestamps.TryPeek(out var earliest) && DateTime.Now - earliest >= refreshSpan) - timestamps.Dequeue(); - } + base.Update(); updateCounter(); - - base.Update(); } private void updateCounter() { - valid.Value = timestamps != null; - Current.Value = timestamps?.Count ?? 0; + valid.Value = timestamps != null && MathHelper.ApproximatelyEquivalent(gameplayClock.CurrentTime, referenceClock.CurrentTime, 500); + Current.Value = timestamps?.Count(timestamp => clock.CurrentTime - timestamp is >= 0 and <= invalidation_timeout) ?? 0; } protected override IHasText CreateText() => new TextComponent diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs index b6094726c0c5..aaf2e997f2d7 100644 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/KeyCounterDisplay.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Graphics; @@ -65,6 +66,7 @@ public override void Add(KeyCounter key) [BackgroundDependencyLoader] private void load(OsuConfigManager config) { + KeysPerSecondCounter.Reset(); config.BindWith(OsuSetting.KeyOverlay, configVisibility); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9a058e45c586..fb2f5566115f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,6 +34,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; @@ -1044,6 +1045,9 @@ public override bool OnExiting(ScreenExitEvent e) musicController.ResetTrackAdjustments(); fadeOut(); + + KeysPerSecondCounter.Reset(); + return base.OnExiting(e); } From 42d1bdfc95fe78555fd5ce0426bc79b6a4d6f736 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Fri, 5 Aug 2022 04:17:01 +0200 Subject: [PATCH 04/38] Move KPS calculation to a standalone class --- .../Gameplay/TestSceneKeyPerSecondCounter.cs | 91 +++++++++++++++++++ .../Gameplay/TestSceneKeysPerSecondCounter.cs | 4 +- .../HUD/KPSCounter/KeysPerSecondCalculator.cs | 91 +++++++++++++++++++ .../{ => KPSCounter}/KeysPerSecondCounter.cs | 63 ++----------- osu.Game/Screens/Play/KeyCounter.cs | 4 +- osu.Game/Screens/Play/KeyCounterDisplay.cs | 2 - osu.Game/Screens/Play/Player.cs | 4 +- 7 files changed, 195 insertions(+), 64 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneKeyPerSecondCounter.cs create mode 100644 osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs rename osu.Game/Screens/Play/HUD/{ => KPSCounter}/KeysPerSecondCounter.cs (63%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyPerSecondCounter.cs new file mode 100644 index 000000000000..a2eaea29ebb8 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyPerSecondCounter.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD.KPSCounter; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneKeyPerSecondCounter : PlayerTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + protected override bool HasCustomSteps => false; + protected override bool Autoplay => true; + + private GameplayClock gameplayClock; + private DrawableRuleset drawableRuleset; + + // private DependencyProvidingContainer dependencyContainer; + private KeysPerSecondCounter counter; + + [SetUpSteps] + public new void SetUpSteps() + { + /* + CreateTest(() => AddStep("Create components", () => + { + Logger.Log($"{(Player != null ? Player.ToString() : "null")}", level: LogLevel.Debug); + dependencyContainer = new DependencyProvidingContainer + { + RelativePositionAxes = Axes.Both, + }; + })); + */ + } + + private void createCounter() + { + AddStep("Create counter", () => + { + /* + if (!Contains(dependencyContainer)) + { + Add(dependencyContainer); + } + + if (dependencyContainer.CachedDependencies.Length == 0) + { + dependencyContainer.CachedDependencies = new (Type, object)[] + { + (typeof(GameplayClock), , + (typeof(DrawableRuleset),) + }; + } + Dependencies.Cache(gameplayClock = Player.GameplayClockContainer.GameplayClock)); + */ + + Dependencies.Cache(gameplayClock = Player.GameplayClockContainer.GameplayClock); + Dependencies.Cache(drawableRuleset = Player.DrawableRuleset); + + Add(counter = new KeysPerSecondCounter + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Scale = new Vector2(5), + Position = new Vector2(10, 100) + } + ); + }); + AddAssert("ensure counter added", () => Contains(counter)); + } + + [Test] + public void TestInGameTimeConsistency() + { + createCounter(); + + AddUntilStep("Wait until first note", () => counter.Current.Value != 0); + AddStep("Pause gameplay", () => gameplayClock.IsPaused.Value = true); + AddAssert("KPS = 1", () => counter.Current.Value == 1); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs index e20a83b54a76..c8c31d13661e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.KPSCounter; using osuTK; using osuTK.Input; @@ -37,7 +37,7 @@ private void createCounter() => AddStep("Create counter", () => public void TestManualTrigger() { AddAssert("Counter = 0", () => counter.Current.Value == 0); - AddRepeatStep("manual trigger", KeysPerSecondCounter.AddTimestamp, 20); + AddRepeatStep("manual trigger", KeysPerSecondCalculator.AddInput, 20); AddAssert("Counter is not 0", () => counter.Current.Value > 0); } diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs new file mode 100644 index 000000000000..f9839abde4a1 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs @@ -0,0 +1,91 @@ +// 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; +using osu.Framework.Timing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Screens.Play.HUD.KPSCounter +{ + public class KeysPerSecondCalculator : IDisposable + { + private static KeysPerSecondCalculator instance; + + public static void AddInput() + { + instance?.onNewInput.Invoke(); + } + + public static KeysPerSecondCalculator GetInstance(GameplayClock gameplayClock = null, DrawableRuleset drawableRuleset = null) + { + if (instance != null) return instance; + + try + { + return new KeysPerSecondCalculator(gameplayClock, drawableRuleset); + } + catch (ArgumentNullException) + { + return null; + } + } + + private readonly List timestamps; + private readonly GameplayClock gameplayClock; + private readonly DrawableRuleset drawableRuleset; + + private event Action onNewInput; + + private IClock workingClock => (IClock)drawableRuleset.FrameStableClock ?? gameplayClock; + + // Having the rate from mods is preffered to using GameplayClock.TrueGameplayRate() + // as it returns 0 when paused in replays, not useful for players who want to "analyze" a replay. + private double rate => (drawableRuleset.Mods.FirstOrDefault(m => m is ModRateAdjust) as ModRateAdjust)?.SpeedChange.Value + ?? 1; + + private double maxTime = double.NegativeInfinity; + + public bool Ready => workingClock != null && gameplayClock != null; + public int Value => timestamps.Count(isTimestampWithinSpan); + + private KeysPerSecondCalculator(GameplayClock gameplayClock, DrawableRuleset drawableRuleset) + { + instance = this; + timestamps = new List(); + this.gameplayClock = gameplayClock ?? throw new ArgumentNullException(nameof(gameplayClock)); + this.drawableRuleset = drawableRuleset; + onNewInput += addTimestamp; + } + + private void addTimestamp() + { + if (workingClock != null && workingClock.CurrentTime >= maxTime) + { + timestamps.Add(workingClock.CurrentTime); + maxTime = workingClock.CurrentTime; + } + } + + private bool isTimestampWithinSpan(double timestamp) + { + if (!Ready) + return false; + + double span = 1000 * rate; + double relativeTime = workingClock.CurrentTime - timestamp; + return relativeTime >= 0 && relativeTime <= span; + } + + public void Dispose() + { + instance = null; + } + + ~KeysPerSecondCalculator() => Dispose(); + } +} diff --git a/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs similarity index 63% rename from osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs rename to osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs index d32ca1410acb..2fcca2ffce76 100644 --- a/osu.Game/Screens/Play/HUD/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs @@ -3,17 +3,12 @@ #nullable disable -using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Framework.Logging; -using osu.Framework.Timing; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -22,28 +17,20 @@ using osu.Game.Skinning; using osuTK; -namespace osu.Game.Screens.Play.HUD +namespace osu.Game.Screens.Play.HUD.KPSCounter { public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable { - private static List timestamps; - private static double maxTime = double.NegativeInfinity; - - private static event Action onNewInput; - - private const int invalidation_timeout = 1000; private const float alpha_when_invalid = 0.3f; private readonly Bindable valid = new Bindable(); - - private static GameplayClock gameplayClock; - private static IClock referenceClock; - - private static IClock clock => referenceClock ?? gameplayClock; + private GameplayClock gameplayClock; [Resolved(canBeNull: true)] private DrawableRuleset drawableRuleset { get; set; } + private KeysPerSecondCalculator calculator => KeysPerSecondCalculator.GetInstance(gameplayClock, drawableRuleset); + [SettingSource("Smoothing time", "How smooth the counter should change\nThe more it is smooth, the less it's accurate.")] public BindableNumber SmoothingTime { get; } = new BindableNumber(350) { @@ -51,66 +38,30 @@ public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable MinValue = 0 }; - public static void AddTimestamp() - { - Logger.Log($"Input timestamp attempt C: {clock.CurrentTime}ms | GC: {gameplayClock.CurrentTime} | RC: {referenceClock?.CurrentTime ?? -1} | Max: {maxTime})", level: LogLevel.Debug); - - if (clock.CurrentTime >= maxTime) - { - Logger.Log("Input timestamp added.", level: LogLevel.Debug); - timestamps?.Add(clock.CurrentTime); - maxTime = timestamps?.Max() ?? clock.CurrentTime; - } - - onNewInput?.Invoke(); - } - - public static void Reset() - { - timestamps?.Clear(); - maxTime = int.MinValue; - } - protected override double RollingDuration => SmoothingTime.Value; public bool UsesFixedAnchor { get; set; } public KeysPerSecondCounter() { - timestamps ??= new List(); Current.Value = 0; - onNewInput += updateCounter; - Scheduler.AddOnce(updateCounter); } [BackgroundDependencyLoader] - private void load(OsuColour colours, GameplayClock clock) + private void load(OsuColour colours, GameplayClock clock, DrawableRuleset ruleset) { gameplayClock = clock; Colour = colours.BlueLighter; valid.BindValueChanged(e => DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); - referenceClock = drawableRuleset?.FrameStableClock; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateCounter(); } protected override void Update() { base.Update(); - updateCounter(); - } - - private void updateCounter() - { - valid.Value = timestamps != null && MathHelper.ApproximatelyEquivalent(gameplayClock.CurrentTime, referenceClock.CurrentTime, 500); - Current.Value = timestamps?.Count(timestamp => clock.CurrentTime - timestamp is >= 0 and <= invalidation_timeout) ?? 0; + valid.Value = calculator.Ready; + Current.Value = calculator.Value; } protected override IHasText CreateText() => new TextComponent diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/KeyCounter.cs index b8bbac9a7e10..044c9ee24e74 100644 --- a/osu.Game/Screens/Play/KeyCounter.cs +++ b/osu.Game/Screens/Play/KeyCounter.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.KPSCounter; using osuTK; using osuTK.Graphics; @@ -56,7 +56,7 @@ protected set public void Increment() { - KeysPerSecondCounter.AddTimestamp(); + KeysPerSecondCalculator.AddInput(); if (!IsCounting) return; diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs index aaf2e997f2d7..b6094726c0c5 100644 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/KeyCounterDisplay.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Configuration; -using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Graphics; @@ -66,7 +65,6 @@ public override void Add(KeyCounter key) [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - KeysPerSecondCounter.Reset(); config.BindWith(OsuSetting.KeyOverlay, configVisibility); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index fb2f5566115f..88cd197076fc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,7 +34,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.KPSCounter; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; @@ -1046,7 +1046,7 @@ public override bool OnExiting(ScreenExitEvent e) fadeOut(); - KeysPerSecondCounter.Reset(); + KeysPerSecondCalculator.GetInstance().Dispose(); return base.OnExiting(e); } From 24c29b7e2f5d6ec717a056db1149099c1632df46 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Fri, 5 Aug 2022 15:51:07 +0200 Subject: [PATCH 05/38] Do not add KPS calculation when gameplay rate is 0 --- osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs index f9839abde4a1..5ac3647e0efa 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs @@ -64,7 +64,7 @@ private KeysPerSecondCalculator(GameplayClock gameplayClock, DrawableRuleset dra private void addTimestamp() { - if (workingClock != null && workingClock.CurrentTime >= maxTime) + if (workingClock != null && workingClock.CurrentTime >= maxTime && gameplayClock.TrueGameplayRate > 0) { timestamps.Add(workingClock.CurrentTime); maxTime = workingClock.CurrentTime; From b4e0fa7c53cf3d1a8d580562c8301a6e70d4d2bc Mon Sep 17 00:00:00 2001 From: Ryuki Date: Fri, 5 Aug 2022 15:53:06 +0200 Subject: [PATCH 06/38] Rewrite tests for KPS --- .../Gameplay/TestSceneKeyPerSecondCounter.cs | 91 --------------- .../Gameplay/TestSceneKeysPerSecondCounter.cs | 108 ++++++++++-------- 2 files changed, 63 insertions(+), 136 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneKeyPerSecondCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyPerSecondCounter.cs deleted file mode 100644 index a2eaea29ebb8..000000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyPerSecondCounter.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mania; -using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.KPSCounter; -using osuTK; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public class TestSceneKeyPerSecondCounter : PlayerTestScene - { - protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); - protected override bool HasCustomSteps => false; - protected override bool Autoplay => true; - - private GameplayClock gameplayClock; - private DrawableRuleset drawableRuleset; - - // private DependencyProvidingContainer dependencyContainer; - private KeysPerSecondCounter counter; - - [SetUpSteps] - public new void SetUpSteps() - { - /* - CreateTest(() => AddStep("Create components", () => - { - Logger.Log($"{(Player != null ? Player.ToString() : "null")}", level: LogLevel.Debug); - dependencyContainer = new DependencyProvidingContainer - { - RelativePositionAxes = Axes.Both, - }; - })); - */ - } - - private void createCounter() - { - AddStep("Create counter", () => - { - /* - if (!Contains(dependencyContainer)) - { - Add(dependencyContainer); - } - - if (dependencyContainer.CachedDependencies.Length == 0) - { - dependencyContainer.CachedDependencies = new (Type, object)[] - { - (typeof(GameplayClock), , - (typeof(DrawableRuleset),) - }; - } - Dependencies.Cache(gameplayClock = Player.GameplayClockContainer.GameplayClock)); - */ - - Dependencies.Cache(gameplayClock = Player.GameplayClockContainer.GameplayClock); - Dependencies.Cache(drawableRuleset = Player.DrawableRuleset); - - Add(counter = new KeysPerSecondCounter - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Scale = new Vector2(5), - Position = new Vector2(10, 100) - } - ); - }); - AddAssert("ensure counter added", () => Contains(counter)); - } - - [Test] - public void TestInGameTimeConsistency() - { - createCounter(); - - AddUntilStep("Wait until first note", () => counter.Current.Value != 0); - AddStep("Pause gameplay", () => gameplayClock.IsPaused.Value = true); - AddAssert("KPS = 1", () => counter.Current.Value == 1); - } - } -} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs index c8c31d13661e..0cc9b91d71b6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs @@ -3,9 +3,15 @@ #nullable disable +using System.Linq; +using AutoMapper.Internal; using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; -using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.KPSCounter; using osuTK; @@ -13,63 +19,75 @@ namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneKeysPerSecondCounter : OsuManualInputManagerTestScene + public class TestSceneKeysPerSecondCounter : PlayerTestScene { - private KeysPerSecondCounter counter; + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + protected override bool HasCustomSteps => false; + protected override bool Autoplay => false; + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false); - [SetUpSteps] - public void Setup() - { - createCounter(); - } + private GameplayClock gameplayClock; + private DrawableRuleset drawableRuleset; - private void createCounter() => AddStep("Create counter", () => + private KeysPerSecondCounter counter; + + private void createCounter() { - Child = counter = new KeysPerSecondCounter + AddStep("Create counter", () => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(5) - }; - }); + gameplayClock = Player.GameplayClockContainer.GameplayClock; + drawableRuleset = Player.DrawableRuleset; - [Test] - public void TestManualTrigger() - { - AddAssert("Counter = 0", () => counter.Current.Value == 0); - AddRepeatStep("manual trigger", KeysPerSecondCalculator.AddInput, 20); - AddAssert("Counter is not 0", () => counter.Current.Value > 0); + Player.HUDOverlay.Add(counter = new KeysPerSecondCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5), + }); + counter.SmoothingTime.Value = 0; + }); + AddUntilStep("Counter created", () => Player.HUDOverlay.Contains(counter)); } [Test] - public void TestKpsAsideKeyCounter() + public void TestBasic() { - AddStep("Create key counter display", () => - Add(new KeyCounterDisplay + createCounter(); + + AddStep("press 1 key", () => InputManager.Key(Key.D)); + AddAssert("KPS = 1", () => counter.Current.Value == 1); + AddUntilStep("Wait for KPS cooldown", () => counter.Current.Value <= 0); + AddStep("press 4 keys", () => + { + InputManager.Key(Key.D); + InputManager.Key(Key.F); + InputManager.Key(Key.J); + InputManager.Key(Key.K); + }); + AddAssert("KPS = 4", () => counter.Current.Value == 4); + AddStep("Pause player", () => Player.Pause()); + AddAssert("KPS = 4", () => counter.Current.Value == 4); + AddStep("Resume player", () => Player.Resume()); + AddStep("press 4 keys", () => + { + InputManager.Key(Key.D); + InputManager.Key(Key.F); + InputManager.Key(Key.J); + InputManager.Key(Key.K); + }); + AddAssert("KPS = 8", () => counter.Current.Value == 8); + AddUntilStep("Wait for KPS cooldown", () => counter.Current.Value <= 0); + AddStep("Add DT", () => + { + var dt = new ManiaModDoubleTime { - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - Y = 100, - Children = new KeyCounter[] + SpeedChange = { - new KeyCounterKeyboard(Key.W), - new KeyCounterKeyboard(Key.X), - new KeyCounterKeyboard(Key.C), - new KeyCounterKeyboard(Key.V) + Value = 2 } - }) - ); - AddAssert("Counter = 0", () => counter.Current.Value == 0); - addPressKeyStep(Key.W); - addPressKeyStep(Key.X); - addPressKeyStep(Key.C); - addPressKeyStep(Key.V); - AddAssert("Counter = 4", () => counter.Current.Value == 4); - } - - private void addPressKeyStep(Key key) - { - AddStep($"Press {key} key", () => InputManager.Key(key)); + }; + Player.Mods.Value.Concat((dt.Yield()).ToArray()); + }); } } } From 0c07df2c267c36fb1c5ac5cf4dae095b94d01521 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Fri, 5 Aug 2022 17:04:33 +0200 Subject: [PATCH 07/38] Remove DT from KPS test --- .../Gameplay/TestSceneKeysPerSecondCounter.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs index 0cc9b91d71b6..6e59c53a1fc2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs @@ -3,14 +3,10 @@ #nullable disable -using System.Linq; -using AutoMapper.Internal; using NUnit.Framework; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; -using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.KPSCounter; @@ -76,18 +72,6 @@ public void TestBasic() InputManager.Key(Key.K); }); AddAssert("KPS = 8", () => counter.Current.Value == 8); - AddUntilStep("Wait for KPS cooldown", () => counter.Current.Value <= 0); - AddStep("Add DT", () => - { - var dt = new ManiaModDoubleTime - { - SpeedChange = - { - Value = 2 - } - }; - Player.Mods.Value.Concat((dt.Yield()).ToArray()); - }); } } } From 0886137e39b965571143387cd44b516b185fd189 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Fri, 5 Aug 2022 16:31:20 +0200 Subject: [PATCH 08/38] Prevent KeysPerSecondCounter from NRE when no instance is initialized --- .../Play/HUD/KPSCounter/KeysPerSecondCalculator.cs | 8 ++++---- osu.Game/Screens/Play/Player.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs index 5ac3647e0efa..a29f4c9706a3 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs @@ -18,7 +18,7 @@ public class KeysPerSecondCalculator : IDisposable public static void AddInput() { - instance?.onNewInput.Invoke(); + instance?.onNewInput?.Invoke(); } public static KeysPerSecondCalculator GetInstance(GameplayClock gameplayClock = null, DrawableRuleset drawableRuleset = null) @@ -41,9 +41,9 @@ public static KeysPerSecondCalculator GetInstance(GameplayClock gameplayClock = private event Action onNewInput; - private IClock workingClock => (IClock)drawableRuleset.FrameStableClock ?? gameplayClock; + private IClock workingClock => (IClock)drawableRuleset?.FrameStableClock ?? gameplayClock; - // Having the rate from mods is preffered to using GameplayClock.TrueGameplayRate() + // Having the rate from mods is preferred to using GameplayClock.TrueGameplayRate() // as it returns 0 when paused in replays, not useful for players who want to "analyze" a replay. private double rate => (drawableRuleset.Mods.FirstOrDefault(m => m is ModRateAdjust) as ModRateAdjust)?.SpeedChange.Value ?? 1; @@ -64,7 +64,7 @@ private KeysPerSecondCalculator(GameplayClock gameplayClock, DrawableRuleset dra private void addTimestamp() { - if (workingClock != null && workingClock.CurrentTime >= maxTime && gameplayClock.TrueGameplayRate > 0) + if (Ready && workingClock.CurrentTime >= maxTime && gameplayClock.TrueGameplayRate > 0) { timestamps.Add(workingClock.CurrentTime); maxTime = workingClock.CurrentTime; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 88cd197076fc..70b1dc9a414a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1046,7 +1046,7 @@ public override bool OnExiting(ScreenExitEvent e) fadeOut(); - KeysPerSecondCalculator.GetInstance().Dispose(); + KeysPerSecondCalculator.GetInstance()?.Dispose(); return base.OnExiting(e); } From b2557a8d2d4ca1ecde0cc6b709085df912316ede Mon Sep 17 00:00:00 2001 From: Ryuki Date: Sun, 7 Aug 2022 00:53:00 +0200 Subject: [PATCH 09/38] Refactor KPS - Remove '#nullable disable' in KeysPerSecondCalculator and KeysPerSecondCounter - Remove KeysPerSecondCalculator IDisposable implementation - Make KeysPerSecondCalculator static instance initialized once by KeysPerSecondCounters - Auto transfer dependencies from KeysPerSecondCounter to KeysPerSecondCalculator using Resolved properties - Add internal reset logic to KeysPerSecondCalculator and make it independent from Player - Use GameplayClock.TrueGameplayRate to get real-time rate. If 0 then it defaults to the last non 0 rate if no such mod is enabled --- .../HUD/KPSCounter/KeysPerSecondCalculator.cs | 96 ++++++++++++------- .../HUD/KPSCounter/KeysPerSecondCounter.cs | 31 +++--- osu.Game/Screens/Play/Player.cs | 3 - 3 files changed, 75 insertions(+), 55 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs index a29f4c9706a3..3c0d5859846c 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -12,59 +10,93 @@ namespace osu.Game.Screens.Play.HUD.KPSCounter { - public class KeysPerSecondCalculator : IDisposable + public class KeysPerSecondCalculator { - private static KeysPerSecondCalculator instance; - public static void AddInput() { - instance?.onNewInput?.Invoke(); + onNewInput?.Invoke(); } - public static KeysPerSecondCalculator GetInstance(GameplayClock gameplayClock = null, DrawableRuleset drawableRuleset = null) - { - if (instance != null) return instance; + private readonly List timestamps; + private GameplayClock? gameplayClock; + private DrawableRuleset? drawableRuleset; - try + public GameplayClock? GameplayClock + { + get => gameplayClock; + set { - return new KeysPerSecondCalculator(gameplayClock, drawableRuleset); + onResetRequested?.Invoke(); + + if (value != null) + { + gameplayClock = value; + } } - catch (ArgumentNullException) + } + + public DrawableRuleset? DrawableRuleset + { + get => drawableRuleset; + set { - return null; + onResetRequested?.Invoke(); + + if (value != null) + { + drawableRuleset = value; + baseRate = (drawableRuleset.Mods.FirstOrDefault(m => m is ModRateAdjust) as ModRateAdjust)?.SpeedChange.Value + ?? 1; + } } } - private readonly List timestamps; - private readonly GameplayClock gameplayClock; - private readonly DrawableRuleset drawableRuleset; + private static event Action? onNewInput; + private static event Action? onResetRequested; - private event Action onNewInput; + private IClock? workingClock => drawableRuleset?.FrameStableClock; - private IClock workingClock => (IClock)drawableRuleset?.FrameStableClock ?? gameplayClock; + private double baseRate; - // Having the rate from mods is preferred to using GameplayClock.TrueGameplayRate() - // as it returns 0 when paused in replays, not useful for players who want to "analyze" a replay. - private double rate => (drawableRuleset.Mods.FirstOrDefault(m => m is ModRateAdjust) as ModRateAdjust)?.SpeedChange.Value - ?? 1; + private double rate + { + get + { + if (gameplayClock != null) + { + if (gameplayClock.TrueGameplayRate > 0) + { + baseRate = gameplayClock.TrueGameplayRate; + } + } + + return baseRate; + } + } private double maxTime = double.NegativeInfinity; public bool Ready => workingClock != null && gameplayClock != null; public int Value => timestamps.Count(isTimestampWithinSpan); - private KeysPerSecondCalculator(GameplayClock gameplayClock, DrawableRuleset drawableRuleset) + public KeysPerSecondCalculator() { - instance = this; timestamps = new List(); - this.gameplayClock = gameplayClock ?? throw new ArgumentNullException(nameof(gameplayClock)); - this.drawableRuleset = drawableRuleset; onNewInput += addTimestamp; + onResetRequested += cleanUp; + } + + private void cleanUp() + { + timestamps.Clear(); + maxTime = double.NegativeInfinity; } private void addTimestamp() { - if (Ready && workingClock.CurrentTime >= maxTime && gameplayClock.TrueGameplayRate > 0) + if (workingClock == null) return; + + if (workingClock.CurrentTime >= maxTime) { timestamps.Add(workingClock.CurrentTime); maxTime = workingClock.CurrentTime; @@ -73,19 +105,11 @@ private void addTimestamp() private bool isTimestampWithinSpan(double timestamp) { - if (!Ready) - return false; + if (workingClock == null) return false; double span = 1000 * rate; double relativeTime = workingClock.CurrentTime - timestamp; return relativeTime >= 0 && relativeTime <= span; } - - public void Dispose() - { - instance = null; - } - - ~KeysPerSecondCalculator() => Dispose(); } } diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs index 2fcca2ffce76..ad7b6c8f5c0c 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -24,21 +21,24 @@ public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable private const float alpha_when_invalid = 0.3f; private readonly Bindable valid = new Bindable(); - private GameplayClock gameplayClock; - [Resolved(canBeNull: true)] - private DrawableRuleset drawableRuleset { get; set; } + private static readonly KeysPerSecondCalculator calculator = new KeysPerSecondCalculator(); - private KeysPerSecondCalculator calculator => KeysPerSecondCalculator.GetInstance(gameplayClock, drawableRuleset); + [Resolved] + private GameplayClock? gameplayClock + { + get => calculator.GameplayClock; + set => calculator.GameplayClock = value; + } - [SettingSource("Smoothing time", "How smooth the counter should change\nThe more it is smooth, the less it's accurate.")] - public BindableNumber SmoothingTime { get; } = new BindableNumber(350) + [Resolved(canBeNull: true)] + private DrawableRuleset? drawableRuleset { - MaxValue = 1000, - MinValue = 0 - }; + get => calculator.DrawableRuleset; + set => calculator.DrawableRuleset = value; + } - protected override double RollingDuration => SmoothingTime.Value; + protected override double RollingDuration => 350; public bool UsesFixedAnchor { get; set; } @@ -48,9 +48,8 @@ public KeysPerSecondCounter() } [BackgroundDependencyLoader] - private void load(OsuColour colours, GameplayClock clock, DrawableRuleset ruleset) + private void load(OsuColour colours) { - gameplayClock = clock; Colour = colours.BlueLighter; valid.BindValueChanged(e => DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); @@ -61,7 +60,7 @@ protected override void Update() base.Update(); valid.Value = calculator.Ready; - Current.Value = calculator.Value; + Current.Value = calculator.Ready ? calculator.Value : 0; } protected override IHasText CreateText() => new TextComponent diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 70b1dc9a414a..e3844088e2f1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,7 +34,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.HUD.KPSCounter; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; @@ -1046,8 +1045,6 @@ public override bool OnExiting(ScreenExitEvent e) fadeOut(); - KeysPerSecondCalculator.GetInstance()?.Dispose(); - return base.OnExiting(e); } From b52a07c16a6559b3661453f533783ae9abeb7a69 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Mon, 8 Aug 2022 21:27:46 +0200 Subject: [PATCH 10/38] Use DI to provide dependencies for KPS Calculator and improve input gathering KPS Calculator now uses DI to retrieve the clocks. Using `HUDOverlay` it is now cached for `KeysPerSecondCounter`s to resolve it. This also allows to make an "Attach" flow like `KeyCounter`. --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 5 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 37 ++++++++++- .../HUD/KPSCounter/KeysPerSecondCalculator.cs | 66 +++++++++---------- .../HUD/KPSCounter/KeysPerSecondCounter.cs | 20 ++---- osu.Game/Screens/Play/HUDOverlay.cs | 8 ++- osu.Game/Screens/Play/KeyCounter.cs | 2 - 6 files changed, 87 insertions(+), 51 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index f7f62d2af047..b28e3355a4c2 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD.KPSCounter; using osuTK; namespace osu.Game.Rulesets.UI @@ -38,7 +39,7 @@ namespace osu.Game.Rulesets.UI /// Displays an interactive ruleset gameplay instance. /// /// The type of HitObject contained by this DrawableRuleset. - public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter + public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter, ICanAttachKpsCalculator where TObject : HitObject { public override event Action NewResult; @@ -340,6 +341,8 @@ public override void SetReplayScore(Score replayScore) public void Attach(KeyCounterDisplay keyCounter) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); + public void Attach(KeysPerSecondCalculator kps) => (KeyBindingInputManager as ICanAttachKpsCalculator)?.Attach(kps); + /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. /// diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 7c37913576f6..23e64153eba0 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -20,11 +20,12 @@ using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD.KPSCounter; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI { - public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler + public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler, ICanAttachKpsCalculator where T : struct { public readonly KeyBindingContainer KeyBindingContainer; @@ -186,6 +187,35 @@ public void OnReleased(KeyBindingReleaseEvent e) #endregion + #region KPS Counter Attachment + + public void Attach(KeysPerSecondCalculator kps) + { + var listener = new ActionListener(); + + KeyBindingContainer.Add(listener); + + kps.Listener = listener; + } + + public class ActionListener : KeysPerSecondCalculator.InputListener, IKeyBindingHandler + { + public override event Action OnNewInput; + + public bool OnPressed(KeyBindingPressEvent e) + { + OnNewInput?.Invoke(); + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } + + #endregion + protected virtual KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) => new RulesetKeyBindingContainer(ruleset, variant, unique); @@ -229,6 +259,11 @@ public interface ICanAttachKeyCounter void Attach(KeyCounterDisplay keyCounter); } + public interface ICanAttachKpsCalculator + { + void Attach(KeysPerSecondCalculator keysPerSecondCalculator); + } + public class RulesetInputManagerInputState : InputState where T : struct { diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs index 3c0d5859846c..96a6d5b8ebfd 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs @@ -4,55 +4,36 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Timing; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.KPSCounter { - public class KeysPerSecondCalculator + public class KeysPerSecondCalculator : Component { - public static void AddInput() - { - onNewInput?.Invoke(); - } - private readonly List timestamps; - private GameplayClock? gameplayClock; - private DrawableRuleset? drawableRuleset; - public GameplayClock? GameplayClock - { - get => gameplayClock; - set - { - onResetRequested?.Invoke(); + private InputListener? listener; - if (value != null) - { - gameplayClock = value; - } - } - } + [Resolved] + private GameplayClock? gameplayClock { get; set; } + + [Resolved(canBeNull: true)] + private DrawableRuleset? drawableRuleset { get; set; } - public DrawableRuleset? DrawableRuleset + public InputListener Listener { - get => drawableRuleset; set { onResetRequested?.Invoke(); - - if (value != null) - { - drawableRuleset = value; - baseRate = (drawableRuleset.Mods.FirstOrDefault(m => m is ModRateAdjust) as ModRateAdjust)?.SpeedChange.Value - ?? 1; - } + listener = value; + listener.OnNewInput += addTimestamp; } } - private static event Action? onNewInput; - private static event Action? onResetRequested; + private event Action? onResetRequested; private IClock? workingClock => drawableRuleset?.FrameStableClock; @@ -81,8 +62,8 @@ private double rate public KeysPerSecondCalculator() { + RelativeSizeAxes = Axes.Both; timestamps = new List(); - onNewInput += addTimestamp; onResetRequested += cleanUp; } @@ -90,6 +71,9 @@ private void cleanUp() { timestamps.Clear(); maxTime = double.NegativeInfinity; + + if (listener != null) + listener.OnNewInput -= addTimestamp; } private void addTimestamp() @@ -111,5 +95,21 @@ private bool isTimestampWithinSpan(double timestamp) double relativeTime = workingClock.CurrentTime - timestamp; return relativeTime >= 0 && relativeTime <= span; } + + ~KeysPerSecondCalculator() + { + cleanUp(); + } + + public abstract class InputListener : Component + { + protected InputListener() + { + RelativeSizeAxes = Axes.Both; + Depth = float.MinValue; + } + + public abstract event Action? OnNewInput; + } } } diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs index ad7b6c8f5c0c..d6f1d19770d0 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs @@ -22,21 +22,15 @@ public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable private readonly Bindable valid = new Bindable(); - private static readonly KeysPerSecondCalculator calculator = new KeysPerSecondCalculator(); + [Resolved] + private KeysPerSecondCalculator? calculator { get; set; } + // This is to force the skin editor to show the component only in a Gameplay context [Resolved] - private GameplayClock? gameplayClock - { - get => calculator.GameplayClock; - set => calculator.GameplayClock = value; - } + private GameplayClock? gameplayClock { get; set; } [Resolved(canBeNull: true)] - private DrawableRuleset? drawableRuleset - { - get => calculator.DrawableRuleset; - set => calculator.DrawableRuleset = value; - } + private DrawableRuleset? drawableRuleset { get; set; } protected override double RollingDuration => 350; @@ -59,8 +53,8 @@ protected override void Update() { base.Update(); - valid.Value = calculator.Ready; - Current.Value = calculator.Ready ? calculator.Value : 0; + valid.Value = calculator != null && calculator.Ready; + Current.Value = calculator != null ? calculator.Ready ? calculator.Value : 0 : 0; } protected override IHasText CreateText() => new TextComponent diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 8f80644d5250..1c28e0495078 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.KPSCounter; using osu.Game.Skinning; using osuTK; @@ -49,6 +50,9 @@ public class HUDOverlay : Container, IKeyBindingHandler public readonly HoldForMenuButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; + [Cached] + private readonly KeysPerSecondCalculator keysPerSecondCalculator; + public Bindable ShowHealthBar = new Bindable(true); private readonly DrawableRuleset drawableRuleset; @@ -122,7 +126,8 @@ public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods) KeyCounter = CreateKeyCounter(), HoldToQuit = CreateHoldForMenuButton(), } - } + }, + keysPerSecondCalculator = new KeysPerSecondCalculator() }; } @@ -260,6 +265,7 @@ private void replayLoadedValueChanged(ValueChangedEvent e) protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { (drawableRuleset as ICanAttachKeyCounter)?.Attach(KeyCounter); + (drawableRuleset as ICanAttachKpsCalculator)?.Attach(keysPerSecondCalculator); replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); } diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/KeyCounter.cs index 044c9ee24e74..1e5ada529570 100644 --- a/osu.Game/Screens/Play/KeyCounter.cs +++ b/osu.Game/Screens/Play/KeyCounter.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Play.HUD.KPSCounter; using osuTK; using osuTK.Graphics; @@ -56,7 +55,6 @@ protected set public void Increment() { - KeysPerSecondCalculator.AddInput(); if (!IsCounting) return; From edb8e5e33e3649f0be60e8b4a53498ab200076a4 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Tue, 9 Aug 2022 02:43:41 +0200 Subject: [PATCH 11/38] Temporarily emptying `TestSceneKeysPerSecondCounter` until a good test can be found --- .../Gameplay/TestSceneKeysPerSecondCounter.cs | 70 +------------------ 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs index 6e59c53a1fc2..8bc2eae1d493 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs @@ -1,77 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mania; -using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.KPSCounter; -using osuTK; -using osuTK.Input; - namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneKeysPerSecondCounter : PlayerTestScene + public class TestSceneKeysPerSecondCounter : OsuManualInputManagerTestScene { - protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); - protected override bool HasCustomSteps => false; - protected override bool Autoplay => false; - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false); - - private GameplayClock gameplayClock; - private DrawableRuleset drawableRuleset; - - private KeysPerSecondCounter counter; - - private void createCounter() - { - AddStep("Create counter", () => - { - gameplayClock = Player.GameplayClockContainer.GameplayClock; - drawableRuleset = Player.DrawableRuleset; - - Player.HUDOverlay.Add(counter = new KeysPerSecondCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(5), - }); - counter.SmoothingTime.Value = 0; - }); - AddUntilStep("Counter created", () => Player.HUDOverlay.Contains(counter)); - } - - [Test] - public void TestBasic() - { - createCounter(); - - AddStep("press 1 key", () => InputManager.Key(Key.D)); - AddAssert("KPS = 1", () => counter.Current.Value == 1); - AddUntilStep("Wait for KPS cooldown", () => counter.Current.Value <= 0); - AddStep("press 4 keys", () => - { - InputManager.Key(Key.D); - InputManager.Key(Key.F); - InputManager.Key(Key.J); - InputManager.Key(Key.K); - }); - AddAssert("KPS = 4", () => counter.Current.Value == 4); - AddStep("Pause player", () => Player.Pause()); - AddAssert("KPS = 4", () => counter.Current.Value == 4); - AddStep("Resume player", () => Player.Resume()); - AddStep("press 4 keys", () => - { - InputManager.Key(Key.D); - InputManager.Key(Key.F); - InputManager.Key(Key.J); - InputManager.Key(Key.K); - }); - AddAssert("KPS = 8", () => counter.Current.Value == 8); - } } } From 9e80d3f71c01a2e5789348a9237bc1d92c4b6c2b Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 00:42:22 +0200 Subject: [PATCH 12/38] Re-adjust timespan conditions in `KeysPerSecondCalculator` --- osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs index 96a6d5b8ebfd..890404a0aa50 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs @@ -93,7 +93,7 @@ private bool isTimestampWithinSpan(double timestamp) double span = 1000 * rate; double relativeTime = workingClock.CurrentTime - timestamp; - return relativeTime >= 0 && relativeTime <= span; + return relativeTime > 0 && relativeTime <= span; } ~KeysPerSecondCalculator() From 46e372cb99ba47a86f17418353086e95a4ddd3fd Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 00:43:15 +0200 Subject: [PATCH 13/38] Add more readiness checks in `KeysPerSecondCalculator` --- osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs index 890404a0aa50..dddef9abafa5 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs @@ -57,7 +57,7 @@ private double rate private double maxTime = double.NegativeInfinity; - public bool Ready => workingClock != null && gameplayClock != null; + public bool Ready => workingClock != null && gameplayClock != null && listener != null; public int Value => timestamps.Count(isTimestampWithinSpan); public KeysPerSecondCalculator() From 0a94fb4039df50a5a48e55cae546221557fcff4d Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 00:46:31 +0200 Subject: [PATCH 14/38] Make KPS counter strictly depending only on KPS calculator `KeysPerSecondCounter` now depends on `KeysPerSecondCalculator` via the `BackgroundDependencyLoaderAttribute` method, making it appear only in a gameplay context without requiring `GameplayClock` without using it. --- .../Play/HUD/KPSCounter/KeysPerSecondCounter.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs index d6f1d19770d0..47ebede62324 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.UI; +// using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; @@ -22,15 +22,7 @@ public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable private readonly Bindable valid = new Bindable(); - [Resolved] - private KeysPerSecondCalculator? calculator { get; set; } - - // This is to force the skin editor to show the component only in a Gameplay context - [Resolved] - private GameplayClock? gameplayClock { get; set; } - - [Resolved(canBeNull: true)] - private DrawableRuleset? drawableRuleset { get; set; } + private KeysPerSecondCalculator? calculator; protected override double RollingDuration => 350; @@ -42,8 +34,9 @@ public KeysPerSecondCounter() } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, KeysPerSecondCalculator calculator) { + this.calculator = calculator; Colour = colours.BlueLighter; valid.BindValueChanged(e => DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); From d58d5eebe2ec52e19142442dcdfd162d6263f213 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 00:51:13 +0200 Subject: [PATCH 15/38] Add basic tests for KPS Created private mock classes to use them in place of `GameplayClock` and `DrawableRuleset`. --- .../Visual/Gameplay/TestSceneKeysPerSecond.cs | 306 ++++++++++++++++++ .../Gameplay/TestSceneKeysPerSecondCounter.cs | 9 - 2 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs new file mode 100644 index 000000000000..4bda998c496c --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs @@ -0,0 +1,306 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +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.Judgements; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD.KPSCounter; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneKeysPerSecond : OsuTestScene + { + private DependencyProvidingContainer? dependencyContainer; + private MockFrameStableClock? mainClock; + private KeysPerSecondCalculator? calculator; + private ManualInputListener? listener; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create components", () => + { + var ruleset = CreateRuleset(); + + Debug.Assert(ruleset != null); + + Children = new Drawable[] + { + dependencyContainer = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(GameplayClock), mainClock = new MockFrameStableClock(new MockFrameBasedClock())), + (typeof(DrawableRuleset), new DrawableCookieziRuleset(ruleset, mainClock)) + } + }, + }; + }); + } + + private void createCalculator() + { + AddStep("create calculator", () => + { + dependencyContainer!.Children = new Drawable[] + { + calculator = new KeysPerSecondCalculator + { + Listener = listener = new ManualInputListener() + }, + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(KeysPerSecondCalculator), calculator) }, + Child = new KeysPerSecondCounter // For visual debugging, has no real purpose in the tests + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5), + } + } + }; + }); + } + + [Test] + public void TestBasicConsistency() + { + createCalculator(); + + AddStep("Create gradually increasing KPS inputs", () => + { + addInputs(generateGraduallyIncreasingKps()); + }); + + for (int i = 0; i < 10; i++) + { + seek(i * 10000); + advanceForwards(2); + int kps = i + 1; + AddAssert($"{kps} KPS", () => calculator!.Value == kps); + } + } + + [Test] + public void TestRateAdjustConsistency() + { + createCalculator(); + + AddStep("Create consistent KPS inputs", () => addInputs(generateConsistentKps(10))); + + advanceForwards(2); + + for (double i = 1; i <= 2; i += 0.25) + { + changeRate(i); + double rate = i; + AddAssert($"KPS approx. = {i}", () => MathHelper.ApproximatelyEquivalent(calculator!.Value, 10 * rate, 0.5)); + } + + for (double i = 1; i >= 0.5; i -= 0.25) + { + changeRate(i); + double rate = i; + AddAssert($"KPS approx. = {i}", () => MathHelper.ApproximatelyEquivalent(calculator!.Value, 10 * rate, 0.5)); + } + } + + [Test] + public void TestInputsDiscardedOnRewind() + { + createCalculator(); + + AddStep("Create consistent KPS inputs", () => addInputs(generateConsistentKps(10))); + seek(1000); + + AddAssert("KPS = 10", () => calculator!.Value == 10); + + AddStep("Create delayed inputs", () => addInputs(generateConsistentKps(10, 50))); + seek(1000); + AddAssert("KPS didn't changed", () => calculator!.Value == 10); + } + + private void seek(double time) => AddStep($"Seek main clock to {time}ms", () => mainClock?.Seek(time)); + + private void changeRate(double rate) => AddStep($"Change rate to x{rate}", () => + (mainClock?.UnderlyingClock as MockFrameBasedClock)!.Rate = rate); + + private void advanceForwards(int frames = 1) => AddStep($"Advance main clock {frames} frame(s) forward.", () => + { + if (mainClock == null) return; + + MockFrameBasedClock underlyingClock = (MockFrameBasedClock)mainClock.UnderlyingClock; + underlyingClock.Backwards = false; + + for (int i = 0; i < frames; i++) + { + underlyingClock.ProcessFrame(); + } + }); + + private void addInputs(IEnumerable inputs) + { + Debug.Assert(mainClock != null && listener != null); + if (!inputs.Any()) return; + + double baseTime = mainClock.CurrentTime; + + foreach (double timestamp in inputs) + { + mainClock.Seek(timestamp); + listener.AddInput(); + } + + mainClock.Seek(baseTime); + } + + private IEnumerable generateGraduallyIncreasingKps() + { + IEnumerable? final = null; + + for (int i = 1; i <= 10; i++) + { + var currentKps = generateConsistentKps(i, (i - 1) * 10000); + + if (i == 1) + { + final = currentKps; + continue; + } + + final = final!.Concat(currentKps); + } + + return final!; + } + + private IEnumerable generateConsistentKps(double kps, double start = 0, double duration = 10) + { + double end = start + 1000 * duration; + + for (; start < end; start += 1000 / kps) + { + yield return start; + } + } + + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + + #region Mock classes + + private class ManualInputListener : KeysPerSecondCalculator.InputListener + { + public override event Action? OnNewInput; + + public void AddInput() => OnNewInput?.Invoke(); + } + + private class MockFrameBasedClock : ManualClock, IFrameBasedClock + { + public const double FRAME_INTERVAL = 1000; + public bool Backwards; + + public MockFrameBasedClock() + { + Rate = 1; + IsRunning = true; + } + + public void ProcessFrame() + { + CurrentTime += FRAME_INTERVAL * Rate * (Backwards ? -1 : 1); + TimeInfo = new FrameTimeInfo + { + Current = CurrentTime, + Elapsed = FRAME_INTERVAL * Rate * (Backwards ? -1 : 1) + }; + } + + public void Seek(double time) + { + TimeInfo = new FrameTimeInfo + { + Elapsed = time - CurrentTime, + Current = CurrentTime = time + }; + } + + public double ElapsedFrameTime => TimeInfo.Elapsed; + public double FramesPerSecond => 1 / FRAME_INTERVAL; + public FrameTimeInfo TimeInfo { get; private set; } + } + + private class MockFrameStableClock : GameplayClock, IFrameStableClock + { + public MockFrameStableClock(MockFrameBasedClock underlyingClock) + : base(underlyingClock) + { + } + + public void Seek(double time) => (UnderlyingClock as MockFrameBasedClock)?.Seek(time); + + public IBindable IsCatchingUp => new Bindable(); + public IBindable WaitingOnFrames => new Bindable(); + } + + private class DrawableCookieziRuleset : DrawableRuleset + { + public DrawableCookieziRuleset(Ruleset ruleset, IFrameStableClock clock) + : base(ruleset) + { + FrameStableClock = clock; + } + +#pragma warning disable CS0067 + public override event Action? NewResult; + public override event Action? RevertResult; +#pragma warning restore CS0067 + public override Playfield? Playfield => null; + public override Container? Overlays => null; + public override Container? FrameStableComponents => null; + public override IFrameStableClock FrameStableClock { get; } + + internal override bool FrameStablePlayback { get; set; } + public override IReadOnlyList Mods => Array.Empty(); + public override IEnumerable Objects => Array.Empty(); + public override double GameplayStartTime => 0; + public override GameplayCursorContainer? Cursor => null; + + public override void SetReplayScore(Score replayScore) + { + } + + public override void SetRecordTarget(Score score) + { + } + + public override void RequestResume(Action continueResume) + { + } + + public override void CancelResume() + { + } + } + + #endregion + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs deleted file mode 100644 index 8bc2eae1d493..000000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecondCounter.cs +++ /dev/null @@ -1,9 +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.Tests.Visual.Gameplay -{ - public class TestSceneKeysPerSecondCounter : OsuManualInputManagerTestScene - { - } -} From 0e1efbd865c96a2aa53c98f80cc1b00d287f2190 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 01:03:47 +0200 Subject: [PATCH 16/38] Rename `DrawableCookieziRuleset` to `MockDrawableRuleset` --- osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs index 4bda998c496c..edfba7f15405 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs @@ -48,7 +48,7 @@ public void SetUpSteps() CachedDependencies = new (Type, object)[] { (typeof(GameplayClock), mainClock = new MockFrameStableClock(new MockFrameBasedClock())), - (typeof(DrawableRuleset), new DrawableCookieziRuleset(ruleset, mainClock)) + (typeof(DrawableRuleset), new MockDrawableRuleset(ruleset, mainClock)) } }, }; @@ -261,9 +261,9 @@ public MockFrameStableClock(MockFrameBasedClock underlyingClock) public IBindable WaitingOnFrames => new Bindable(); } - private class DrawableCookieziRuleset : DrawableRuleset + private class MockDrawableRuleset : DrawableRuleset { - public DrawableCookieziRuleset(Ruleset ruleset, IFrameStableClock clock) + public MockDrawableRuleset(Ruleset ruleset, IFrameStableClock clock) : base(ruleset) { FrameStableClock = clock; From 3c6461b9e4383174278cf3b4ab7493a6c076473d Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 09:36:35 +0200 Subject: [PATCH 17/38] Remove KPS acronym usage --- .../Visual/Gameplay/TestSceneKeysPerSecond.cs | 2 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 6 +++--- osu.Game/Rulesets/UI/RulesetInputManager.cs | 16 ++++++---------- .../KeysPerSecondCalculator.cs | 2 +- .../KeysPerSecondCounter.cs | 4 ++-- osu.Game/Screens/Play/HUDOverlay.cs | 5 ++--- 6 files changed, 15 insertions(+), 20 deletions(-) rename osu.Game/Screens/Play/HUD/{KPSCounter => KeysPerSecond}/KeysPerSecondCalculator.cs (98%) rename osu.Game/Screens/Play/HUD/{KPSCounter => KeysPerSecond}/KeysPerSecondCounter.cs (98%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs index edfba7f15405..5c1ca18dbcac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs @@ -19,7 +19,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.KPSCounter; +using osu.Game.Screens.Play.HUD.KeysPerSecond; using osuTK; namespace osu.Game.Tests.Visual.Gameplay diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index b28e3355a4c2..443e4392cfde 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -30,7 +30,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.KPSCounter; +using osu.Game.Screens.Play.HUD.KeysPerSecond; using osuTK; namespace osu.Game.Rulesets.UI @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.UI /// Displays an interactive ruleset gameplay instance. /// /// The type of HitObject contained by this DrawableRuleset. - public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter, ICanAttachKpsCalculator + public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter where TObject : HitObject { public override event Action NewResult; @@ -341,7 +341,7 @@ public override void SetReplayScore(Score replayScore) public void Attach(KeyCounterDisplay keyCounter) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); - public void Attach(KeysPerSecondCalculator kps) => (KeyBindingInputManager as ICanAttachKpsCalculator)?.Attach(kps); + public void Attach(KeysPerSecondCalculator calculator) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(calculator); /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 23e64153eba0..2e9986ada648 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -20,12 +20,12 @@ using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.KPSCounter; +using osu.Game.Screens.Play.HUD.KeysPerSecond; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI { - public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler, ICanAttachKpsCalculator + public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler where T : struct { public readonly KeyBindingContainer KeyBindingContainer; @@ -187,15 +187,15 @@ public void OnReleased(KeyBindingReleaseEvent e) #endregion - #region KPS Counter Attachment + #region Keys per second Counter Attachment - public void Attach(KeysPerSecondCalculator kps) + public void Attach(KeysPerSecondCalculator calculator) { var listener = new ActionListener(); KeyBindingContainer.Add(listener); - kps.Listener = listener; + calculator.Listener = listener; } public class ActionListener : KeysPerSecondCalculator.InputListener, IKeyBindingHandler @@ -257,11 +257,7 @@ public interface IHasRecordingHandler public interface ICanAttachKeyCounter { void Attach(KeyCounterDisplay keyCounter); - } - - public interface ICanAttachKpsCalculator - { - void Attach(KeysPerSecondCalculator keysPerSecondCalculator); + void Attach(KeysPerSecondCalculator calculator); } public class RulesetInputManagerInputState : InputState diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs similarity index 98% rename from osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs rename to osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs index dddef9abafa5..ecc9c6ef86b5 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs @@ -9,7 +9,7 @@ using osu.Framework.Timing; using osu.Game.Rulesets.UI; -namespace osu.Game.Screens.Play.HUD.KPSCounter +namespace osu.Game.Screens.Play.HUD.KeysPerSecond { public class KeysPerSecondCalculator : Component { diff --git a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs similarity index 98% rename from osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs rename to osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs index 47ebede62324..6589dbb71991 100644 --- a/osu.Game/Screens/Play/HUD/KPSCounter/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs @@ -10,11 +10,11 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -// using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; +// using osu.Game.Rulesets.UI; -namespace osu.Game.Screens.Play.HUD.KPSCounter +namespace osu.Game.Screens.Play.HUD.KeysPerSecond { public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 1c28e0495078..20a1a27f3d1b 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -22,7 +22,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Play.HUD.KPSCounter; +using osu.Game.Screens.Play.HUD.KeysPerSecond; using osu.Game.Skinning; using osuTK; @@ -127,7 +127,6 @@ public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods) HoldToQuit = CreateHoldForMenuButton(), } }, - keysPerSecondCalculator = new KeysPerSecondCalculator() }; } @@ -265,7 +264,7 @@ private void replayLoadedValueChanged(ValueChangedEvent e) protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { (drawableRuleset as ICanAttachKeyCounter)?.Attach(KeyCounter); - (drawableRuleset as ICanAttachKpsCalculator)?.Attach(keysPerSecondCalculator); + (drawableRuleset as ICanAttachKeyCounter)?.Attach(keysPerSecondCalculator); replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); } From 787dee249da553736e006485582a8eaddcf54ef5 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 10:37:06 +0200 Subject: [PATCH 18/38] Move `KeysPerSecondCalculator` instantiation from `HUDOverlay` to `Player` This prevents messing with *future* Skin (de)serialization --- osu.Game/Screens/Play/HUDOverlay.cs | 7 ++++--- osu.Game/Screens/Play/Player.cs | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 20a1a27f3d1b..1cddbcac4145 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -50,8 +50,7 @@ public class HUDOverlay : Container, IKeyBindingHandler public readonly HoldForMenuButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; - [Cached] - private readonly KeysPerSecondCalculator keysPerSecondCalculator; + private KeysPerSecondCalculator keysPerSecondCalculator; public Bindable ShowHealthBar = new Bindable(true); @@ -131,8 +130,10 @@ public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods) } [BackgroundDependencyLoader(true)] - private void load(OsuConfigManager config, INotificationOverlay notificationOverlay) + private void load(OsuConfigManager config, INotificationOverlay notificationOverlay, KeysPerSecondCalculator calculator) { + keysPerSecondCalculator = calculator; + if (drawableRuleset != null) { BindDrawableRuleset(drawableRuleset); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e3844088e2f1..ba7e01a8031b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,6 +34,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play.HUD.KeysPerSecond; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; @@ -122,6 +123,8 @@ public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisab private SkipOverlay skipIntroOverlay; private SkipOverlay skipOutroOverlay; + protected KeysPerSecondCalculator KeysPerSecondCalculator { get; private set; } + protected ScoreProcessor ScoreProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; } @@ -226,6 +229,9 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game, dependencies.CacheAs(ScoreProcessor); + KeysPerSecondCalculator = new KeysPerSecondCalculator(); + dependencies.CacheAs(KeysPerSecondCalculator); + HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); @@ -442,6 +448,7 @@ private Drawable createOverlayComponents(IWorkingBeatmap working) OnRetry = Restart, OnQuit = () => PerformExit(true), }, + KeysPerSecondCalculator }, }; From d29cba80e9705a2a28c4dff23c0dd55312923654 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 11:01:16 +0200 Subject: [PATCH 19/38] Remove useless comment in `KeysPerSecondCounter` --- osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs index 6589dbb71991..7bd4d4124262 100644 --- a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Skinning; using osuTK; -// using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.KeysPerSecond { From 9b252b1d81ce40307324e2a3197433dd874d464c Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 11 Aug 2022 11:58:30 +0200 Subject: [PATCH 20/38] Make `KeysPerSecondCalculator` dependency in `HUDOverlay` nullable --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 2 ++ osu.Game/Screens/Play/HUDOverlay.cs | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 2e9986ada648..590a305f775e 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -191,6 +191,8 @@ public void OnReleased(KeyBindingReleaseEvent e) public void Attach(KeysPerSecondCalculator calculator) { + if (calculator == null) return; + var listener = new ActionListener(); KeyBindingContainer.Add(listener); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 1cddbcac4145..458e19826b00 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -50,7 +50,8 @@ public class HUDOverlay : Container, IKeyBindingHandler public readonly HoldForMenuButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; - private KeysPerSecondCalculator keysPerSecondCalculator; + [Resolved(canBeNull: true)] + private KeysPerSecondCalculator keysPerSecondCalculator { get; set; } public Bindable ShowHealthBar = new Bindable(true); @@ -130,10 +131,8 @@ public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods) } [BackgroundDependencyLoader(true)] - private void load(OsuConfigManager config, INotificationOverlay notificationOverlay, KeysPerSecondCalculator calculator) + private void load(OsuConfigManager config, INotificationOverlay notificationOverlay) { - keysPerSecondCalculator = calculator; - if (drawableRuleset != null) { BindDrawableRuleset(drawableRuleset); From e08f71797ec89bbcf41be2689a6f9d939623d4ba Mon Sep 17 00:00:00 2001 From: Ryuki Date: Sat, 13 Aug 2022 04:27:26 +0200 Subject: [PATCH 21/38] Change displayed metric from "KPS" to "clicks/s" --- .../HUD/KeysPerSecond/KeysPerSecondCounter.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs index 7bd4d4124262..a5c122f5b18e 100644 --- a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs @@ -80,13 +80,30 @@ public TextComponent() Origin = Anchor.BottomLeft, Font = OsuFont.Numeric.With(size: 16, fixedWidth: true) }, - new OsuSpriteText + new FillFlowContainer { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Font = OsuFont.Numeric.With(size: 8, fixedWidth: true), - Text = @"KPS", - Padding = new MarginPadding { Bottom = 1.5f }, // align baseline better + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Font = OsuFont.Numeric.With(size: 6, fixedWidth: false), + Text = @"clicks", + }, + new OsuSpriteText + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Font = OsuFont.Numeric.With(size: 6, fixedWidth: false), + Text = @"/sec", + Padding = new MarginPadding { Bottom = 3f }, // align baseline better + } + } } } }; From d5f10cbb9d1396ab10c32981c436729fb4c2c639 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Sun, 14 Aug 2022 18:53:00 +0200 Subject: [PATCH 22/38] Revert 787dee24 and initialize calculator in `HUDOverlay` --- osu.Game/Screens/Play/HUDOverlay.cs | 5 +++-- osu.Game/Screens/Play/Player.cs | 7 ------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 458e19826b00..20f7f7d6c2c1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -50,8 +50,8 @@ public class HUDOverlay : Container, IKeyBindingHandler public readonly HoldForMenuButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; - [Resolved(canBeNull: true)] - private KeysPerSecondCalculator keysPerSecondCalculator { get; set; } + [Cached] + private readonly KeysPerSecondCalculator keysPerSecondCalculator; public Bindable ShowHealthBar = new Bindable(true); @@ -127,6 +127,7 @@ public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods) HoldToQuit = CreateHoldForMenuButton(), } }, + keysPerSecondCalculator = new KeysPerSecondCalculator() }; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ba7e01a8031b..e3844088e2f1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -34,7 +34,6 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.HUD.KeysPerSecond; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; @@ -123,8 +122,6 @@ public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisab private SkipOverlay skipIntroOverlay; private SkipOverlay skipOutroOverlay; - protected KeysPerSecondCalculator KeysPerSecondCalculator { get; private set; } - protected ScoreProcessor ScoreProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; } @@ -229,9 +226,6 @@ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game, dependencies.CacheAs(ScoreProcessor); - KeysPerSecondCalculator = new KeysPerSecondCalculator(); - dependencies.CacheAs(KeysPerSecondCalculator); - HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); @@ -448,7 +442,6 @@ private Drawable createOverlayComponents(IWorkingBeatmap working) OnRetry = Restart, OnQuit = () => PerformExit(true), }, - KeysPerSecondCalculator }, }; From 9dc806506e55ca13b39241b50e71a1b7da7a332b Mon Sep 17 00:00:00 2001 From: Ryuki Date: Sun, 14 Aug 2022 19:09:34 +0200 Subject: [PATCH 23/38] Make `ActionListener` and `KeysPerSecondCalculator` not rely on events to add timestamps --- .../Visual/Gameplay/TestSceneKeysPerSecond.cs | 13 +++++----- osu.Game/Rulesets/UI/RulesetInputManager.cs | 11 +++++--- .../KeysPerSecond/KeysPerSecondCalculator.cs | 25 ++++++------------- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs index 5c1ca18dbcac..ac7b7521c97e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs @@ -61,10 +61,7 @@ private void createCalculator() { dependencyContainer!.Children = new Drawable[] { - calculator = new KeysPerSecondCalculator - { - Listener = listener = new ManualInputListener() - }, + calculator = new KeysPerSecondCalculator(), new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, @@ -77,6 +74,7 @@ private void createCalculator() } } }; + calculator!.Listener = listener = new ManualInputListener(calculator!); }); } @@ -208,9 +206,12 @@ private IEnumerable generateConsistentKps(double kps, double start = 0, private class ManualInputListener : KeysPerSecondCalculator.InputListener { - public override event Action? OnNewInput; + public void AddInput() => Calculator.AddTimestamp(); - public void AddInput() => OnNewInput?.Invoke(); + public ManualInputListener(KeysPerSecondCalculator calculator) + : base(calculator) + { + } } private class MockFrameBasedClock : ManualClock, IFrameBasedClock diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 590a305f775e..23580bc40a61 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -193,7 +193,7 @@ public void Attach(KeysPerSecondCalculator calculator) { if (calculator == null) return; - var listener = new ActionListener(); + var listener = new ActionListener(calculator); KeyBindingContainer.Add(listener); @@ -202,11 +202,9 @@ public void Attach(KeysPerSecondCalculator calculator) public class ActionListener : KeysPerSecondCalculator.InputListener, IKeyBindingHandler { - public override event Action OnNewInput; - public bool OnPressed(KeyBindingPressEvent e) { - OnNewInput?.Invoke(); + Calculator.AddTimestamp(); return false; } @@ -214,6 +212,11 @@ public bool OnPressed(KeyBindingPressEvent e) public void OnReleased(KeyBindingReleaseEvent e) { } + + public ActionListener(KeysPerSecondCalculator calculator) + : base(calculator) + { + } } #endregion diff --git a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs index ecc9c6ef86b5..20ab09e9cc91 100644 --- a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs @@ -29,7 +29,6 @@ public InputListener Listener { onResetRequested?.Invoke(); listener = value; - listener.OnNewInput += addTimestamp; } } @@ -43,12 +42,9 @@ private double rate { get { - if (gameplayClock != null) + if (gameplayClock?.TrueGameplayRate > 0) { - if (gameplayClock.TrueGameplayRate > 0) - { - baseRate = gameplayClock.TrueGameplayRate; - } + baseRate = gameplayClock.TrueGameplayRate; } return baseRate; @@ -71,12 +67,9 @@ private void cleanUp() { timestamps.Clear(); maxTime = double.NegativeInfinity; - - if (listener != null) - listener.OnNewInput -= addTimestamp; } - private void addTimestamp() + public void AddTimestamp() { if (workingClock == null) return; @@ -96,20 +89,16 @@ private bool isTimestampWithinSpan(double timestamp) return relativeTime > 0 && relativeTime <= span; } - ~KeysPerSecondCalculator() - { - cleanUp(); - } - public abstract class InputListener : Component { - protected InputListener() + protected KeysPerSecondCalculator Calculator; + + protected InputListener(KeysPerSecondCalculator calculator) { RelativeSizeAxes = Axes.Both; Depth = float.MinValue; + Calculator = calculator; } - - public abstract event Action? OnNewInput; } } } From 2aa3a1b50d558b8c01097b7c601889848596d8f6 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Sun, 14 Aug 2022 20:12:11 +0200 Subject: [PATCH 24/38] Rename all "KeysPerSecond" usages to "ClicksPerSecond" --- ...sPerSecond.cs => TestSceneClicksPerSecond.cs} | 16 ++++++++-------- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 ++-- osu.Game/Rulesets/UI/RulesetInputManager.cs | 10 +++++----- .../ClicksPerSecondCalculator.cs} | 10 +++++----- .../ClicksPerSecondCounter.cs} | 10 +++++----- osu.Game/Screens/Play/HUDOverlay.cs | 8 ++++---- 6 files changed, 29 insertions(+), 29 deletions(-) rename osu.Game.Tests/Visual/Gameplay/{TestSceneKeysPerSecond.cs => TestSceneClicksPerSecond.cs} (94%) rename osu.Game/Screens/Play/HUD/{KeysPerSecond/KeysPerSecondCalculator.cs => ClicksPerSecond/ClicksPerSecondCalculator.cs} (90%) rename osu.Game/Screens/Play/HUD/{KeysPerSecond/KeysPerSecondCounter.cs => ClicksPerSecond/ClicksPerSecondCounter.cs} (92%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs similarity index 94% rename from osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs index ac7b7521c97e..8a5bd1af0fed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeysPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs @@ -19,16 +19,16 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.KeysPerSecond; +using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osuTK; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneKeysPerSecond : OsuTestScene + public class TestSceneClicksPerSecond : OsuTestScene { private DependencyProvidingContainer? dependencyContainer; private MockFrameStableClock? mainClock; - private KeysPerSecondCalculator? calculator; + private ClicksPerSecondCalculator? calculator; private ManualInputListener? listener; [SetUpSteps] @@ -61,12 +61,12 @@ private void createCalculator() { dependencyContainer!.Children = new Drawable[] { - calculator = new KeysPerSecondCalculator(), + calculator = new ClicksPerSecondCalculator(), new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] { (typeof(KeysPerSecondCalculator), calculator) }, - Child = new KeysPerSecondCounter // For visual debugging, has no real purpose in the tests + CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) }, + Child = new ClicksPerSecondCounter // For visual debugging, has no real purpose in the tests { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -204,11 +204,11 @@ private IEnumerable generateConsistentKps(double kps, double start = 0, #region Mock classes - private class ManualInputListener : KeysPerSecondCalculator.InputListener + private class ManualInputListener : ClicksPerSecondCalculator.InputListener { public void AddInput() => Calculator.AddTimestamp(); - public ManualInputListener(KeysPerSecondCalculator calculator) + public ManualInputListener(ClicksPerSecondCalculator calculator) : base(calculator) { } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 443e4392cfde..cb483bff81af 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -30,7 +30,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.KeysPerSecond; +using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osuTK; namespace osu.Game.Rulesets.UI @@ -341,7 +341,7 @@ public override void SetReplayScore(Score replayScore) public void Attach(KeyCounterDisplay keyCounter) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); - public void Attach(KeysPerSecondCalculator calculator) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(calculator); + public void Attach(ClicksPerSecondCalculator calculator) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(calculator); /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 23580bc40a61..4b7ce22cfccd 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -20,7 +20,7 @@ using osu.Game.Input.Handlers; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD.KeysPerSecond; +using osu.Game.Screens.Play.HUD.ClicksPerSecond; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI @@ -189,7 +189,7 @@ public void OnReleased(KeyBindingReleaseEvent e) #region Keys per second Counter Attachment - public void Attach(KeysPerSecondCalculator calculator) + public void Attach(ClicksPerSecondCalculator calculator) { if (calculator == null) return; @@ -200,7 +200,7 @@ public void Attach(KeysPerSecondCalculator calculator) calculator.Listener = listener; } - public class ActionListener : KeysPerSecondCalculator.InputListener, IKeyBindingHandler + public class ActionListener : ClicksPerSecondCalculator.InputListener, IKeyBindingHandler { public bool OnPressed(KeyBindingPressEvent e) { @@ -213,7 +213,7 @@ public void OnReleased(KeyBindingReleaseEvent e) { } - public ActionListener(KeysPerSecondCalculator calculator) + public ActionListener(ClicksPerSecondCalculator calculator) : base(calculator) { } @@ -262,7 +262,7 @@ public interface IHasRecordingHandler public interface ICanAttachKeyCounter { void Attach(KeyCounterDisplay keyCounter); - void Attach(KeysPerSecondCalculator calculator); + void Attach(ClicksPerSecondCalculator calculator); } public class RulesetInputManagerInputState : InputState diff --git a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs similarity index 90% rename from osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs rename to osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index 20ab09e9cc91..9dc8615fb6b4 100644 --- a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -9,9 +9,9 @@ using osu.Framework.Timing; using osu.Game.Rulesets.UI; -namespace osu.Game.Screens.Play.HUD.KeysPerSecond +namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public class KeysPerSecondCalculator : Component + public class ClicksPerSecondCalculator : Component { private readonly List timestamps; @@ -56,7 +56,7 @@ private double rate public bool Ready => workingClock != null && gameplayClock != null && listener != null; public int Value => timestamps.Count(isTimestampWithinSpan); - public KeysPerSecondCalculator() + public ClicksPerSecondCalculator() { RelativeSizeAxes = Axes.Both; timestamps = new List(); @@ -91,9 +91,9 @@ private bool isTimestampWithinSpan(double timestamp) public abstract class InputListener : Component { - protected KeysPerSecondCalculator Calculator; + protected ClicksPerSecondCalculator Calculator; - protected InputListener(KeysPerSecondCalculator calculator) + protected InputListener(ClicksPerSecondCalculator calculator) { RelativeSizeAxes = Axes.Both; Depth = float.MinValue; diff --git a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs similarity index 92% rename from osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs rename to osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs index a5c122f5b18e..049f38ba4c41 100644 --- a/osu.Game/Screens/Play/HUD/KeysPerSecond/KeysPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs @@ -13,27 +13,27 @@ using osu.Game.Skinning; using osuTK; -namespace osu.Game.Screens.Play.HUD.KeysPerSecond +namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { - public class KeysPerSecondCounter : RollingCounter, ISkinnableDrawable + public class ClicksPerSecondCounter : RollingCounter, ISkinnableDrawable { private const float alpha_when_invalid = 0.3f; private readonly Bindable valid = new Bindable(); - private KeysPerSecondCalculator? calculator; + private ClicksPerSecondCalculator? calculator; protected override double RollingDuration => 350; public bool UsesFixedAnchor { get; set; } - public KeysPerSecondCounter() + public ClicksPerSecondCounter() { Current.Value = 0; } [BackgroundDependencyLoader] - private void load(OsuColour colours, KeysPerSecondCalculator calculator) + private void load(OsuColour colours, ClicksPerSecondCalculator calculator) { this.calculator = calculator; Colour = colours.BlueLighter; diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 20f7f7d6c2c1..b27efaf13a13 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -22,7 +22,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Play.HUD.KeysPerSecond; +using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Skinning; using osuTK; @@ -51,7 +51,7 @@ public class HUDOverlay : Container, IKeyBindingHandler public readonly PlayerSettingsOverlay PlayerSettingsOverlay; [Cached] - private readonly KeysPerSecondCalculator keysPerSecondCalculator; + private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; public Bindable ShowHealthBar = new Bindable(true); @@ -127,7 +127,7 @@ public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods) HoldToQuit = CreateHoldForMenuButton(), } }, - keysPerSecondCalculator = new KeysPerSecondCalculator() + clicksPerSecondCalculator = new ClicksPerSecondCalculator() }; } @@ -265,7 +265,7 @@ private void replayLoadedValueChanged(ValueChangedEvent e) protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { (drawableRuleset as ICanAttachKeyCounter)?.Attach(KeyCounter); - (drawableRuleset as ICanAttachKeyCounter)?.Attach(keysPerSecondCalculator); + (drawableRuleset as ICanAttachKeyCounter)?.Attach(clicksPerSecondCalculator); replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); } From 3de35a15180fd37a72df6ae3bb72d589797f25ef Mon Sep 17 00:00:00 2001 From: Ryuki Date: Thu, 18 Aug 2022 18:40:02 +0200 Subject: [PATCH 25/38] Update calculator and tests to match changes on clocks --- .../Gameplay/TestSceneClicksPerSecond.cs | 22 ++++++++++++++++--- .../ClicksPerSecondCalculator.cs | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs index 8a5bd1af0fed..69868e07b4ee 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs @@ -47,7 +47,7 @@ public void SetUpSteps() RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] { - (typeof(GameplayClock), mainClock = new MockFrameStableClock(new MockFrameBasedClock())), + (typeof(IGameplayClock), mainClock = new MockFrameStableClock(new MockFrameBasedClock())), (typeof(DrawableRuleset), new MockDrawableRuleset(ruleset, mainClock)) } }, @@ -249,17 +249,33 @@ public void Seek(double time) public FrameTimeInfo TimeInfo { get; private set; } } - private class MockFrameStableClock : GameplayClock, IFrameStableClock + private class MockFrameStableClock : IGameplayClock, IFrameStableClock { + internal readonly IFrameBasedClock UnderlyingClock; + + public readonly BindableBool IsPaused = new BindableBool(); + public MockFrameStableClock(MockFrameBasedClock underlyingClock) - : base(underlyingClock) { + UnderlyingClock = underlyingClock; } public void Seek(double time) => (UnderlyingClock as MockFrameBasedClock)?.Seek(time); public IBindable IsCatchingUp => new Bindable(); public IBindable WaitingOnFrames => new Bindable(); + public double CurrentTime => UnderlyingClock.CurrentTime; + public double Rate => UnderlyingClock.Rate; + public bool IsRunning => UnderlyingClock.IsRunning; + public void ProcessFrame() => UnderlyingClock.ProcessFrame(); + + public double ElapsedFrameTime => UnderlyingClock.ElapsedFrameTime; + public double FramesPerSecond => UnderlyingClock.FramesPerSecond; + public FrameTimeInfo TimeInfo => UnderlyingClock.TimeInfo; + public double TrueGameplayRate => UnderlyingClock.Rate; + public double? StartTime => 0; + public IEnumerable NonGameplayAdjustments => Enumerable.Empty(); + IBindable IGameplayClock.IsPaused => IsPaused; } private class MockDrawableRuleset : DrawableRuleset diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index 9dc8615fb6b4..d9c3c6ffec2e 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -18,7 +18,7 @@ public class ClicksPerSecondCalculator : Component private InputListener? listener; [Resolved] - private GameplayClock? gameplayClock { get; set; } + private IGameplayClock? gameplayClock { get; set; } [Resolved(canBeNull: true)] private DrawableRuleset? drawableRuleset { get; set; } From 3ac6500423a8c7607989e855de3eda71484eca8f Mon Sep 17 00:00:00 2001 From: Ryuki Date: Fri, 19 Aug 2022 00:49:03 +0200 Subject: [PATCH 26/38] Add new test resources for CPS counter --- .../Archives/modified-default-20220818.osk | Bin 0 -> 1145 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-default-20220818.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-default-20220818.osk b/osu.Game.Tests/Resources/Archives/modified-default-20220818.osk new file mode 100644 index 0000000000000000000000000000000000000000..92215cbf8655decffeedfabd902ed0e8923bab66 GIT binary patch literal 1145 zcmWIWW@Zs#U|`^2NLVTqs`|jDa|)2R0W8A6P@J8arxn6S=HYqNAcrbLkFt<8S zdHCG=Wxv;FdCR48&5>TQc#^zr@Z(j<8zY0+!z(-V9<9<3Kyi8A6BC6ZpsPE9m=}ma zF3-$M%h$^)&d*!@>}gOyz$foBzM-3f0#51cg@#E_w1`>3Opjh`99S~7VrLd3s^caHu3EB{k%8ea&}Cvk$N45^=6Qs= zIOpdUYt#t>`Il0=~M34Q~2k%>zY_|J@Sq?YP0NlkD**?k?0eZrEh$HT#c&P z9%mv}`qyo)_ritmZ>gHp6`p^0>BQEfRwWm`?gy+5d263H)tddC+uj3ECk}@!ESGR@ALFKi%qnSq2%um-l#P&Pw z=#<$j^rQXL#=;|O+LN{a9{(t3aqe3de_f6Z*MX%+dkuuRR`--M_xVM-9hRSy`v1+3 zDecZRMT{q9mY8*gr!_ZO|AU)E;K zooDlh4K<=Qcl+?N03(VK0p5&EA`G~*98eboG=eBt z21M6|p4_2&7#JFNL3P0sKe|@*#E8%u4osrhlP09~Q;lPaH>i}<7Hjn}qAp8WR J3xP@)7yzY8uFwDg literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index c7eb334f255b..1b03f8ef6bea 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -36,7 +36,9 @@ public class SkinDeserialisationTest "Archives/modified-default-20220723.osk", "Archives/modified-classic-20220723.osk", // Covers legacy song progress, UR counter, colour hit error metre. - "Archives/modified-classic-20220801.osk" + "Archives/modified-classic-20220801.osk", + // Covers clicks/s counter + "Archives/modified-default-20220818.osk" }; /// From 5cf54a788adc5601a518b41409a91a9daca76534 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Sun, 21 Aug 2022 03:15:30 +0200 Subject: [PATCH 27/38] Code cleanup for CPS counter --- .../ClicksPerSecond/ClicksPerSecondCounter.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs index 049f38ba4c41..3ff06d52171e 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -19,9 +18,8 @@ public class ClicksPerSecondCounter : RollingCounter, ISkinnableDrawable { private const float alpha_when_invalid = 0.3f; - private readonly Bindable valid = new Bindable(); - - private ClicksPerSecondCalculator? calculator; + [Resolved(canBeNull: false)] + private ClicksPerSecondCalculator calculator { get; set; } = null!; protected override double RollingDuration => 350; @@ -33,26 +31,19 @@ public ClicksPerSecondCounter() } [BackgroundDependencyLoader] - private void load(OsuColour colours, ClicksPerSecondCalculator calculator) + private void load(OsuColour colours) { - this.calculator = calculator; Colour = colours.BlueLighter; - valid.BindValueChanged(e => - DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint)); } protected override void Update() { base.Update(); - valid.Value = calculator != null && calculator.Ready; - Current.Value = calculator != null ? calculator.Ready ? calculator.Value : 0 : 0; + Current.Value = calculator.Ready ? calculator.Value : 0; } - protected override IHasText CreateText() => new TextComponent - { - Alpha = alpha_when_invalid - }; + protected override IHasText CreateText() => new TextComponent(); private class TextComponent : CompositeDrawable, IHasText { From c56390cd7b4f7f4c442a321e9a71ce1293e72a3d Mon Sep 17 00:00:00 2001 From: Ryuki Date: Mon, 22 Aug 2022 00:03:24 +0200 Subject: [PATCH 28/38] Use less custom classes for CPS tests --- .../Gameplay/TestSceneClicksPerSecond.cs | 246 ++++++++++-------- 1 file changed, 131 insertions(+), 115 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs index 69868e07b4ee..375726dd9a47 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; @@ -13,9 +14,9 @@ using osu.Framework.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; @@ -27,9 +28,12 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSceneClicksPerSecond : OsuTestScene { private DependencyProvidingContainer? dependencyContainer; - private MockFrameStableClock? mainClock; private ClicksPerSecondCalculator? calculator; private ManualInputListener? listener; + private GameplayClockContainer? gameplayClockContainer; + private ManualClock? manualClock; + private DrawableRuleset? drawableRuleset; + private IFrameStableClock? frameStableClock; [SetUpSteps] public void SetUpSteps() @@ -40,41 +44,20 @@ public void SetUpSteps() Debug.Assert(ruleset != null); - Children = new Drawable[] + Child = gameplayClockContainer = new GameplayClockContainer(manualClock = new ManualClock()); + gameplayClockContainer.AddRange(new Drawable[] { + drawableRuleset = new TestDrawableRuleset(frameStableClock = new TestFrameStableClock(manualClock)), dependencyContainer = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, CachedDependencies = new (Type, object)[] { - (typeof(IGameplayClock), mainClock = new MockFrameStableClock(new MockFrameBasedClock())), - (typeof(DrawableRuleset), new MockDrawableRuleset(ruleset, mainClock)) - } - }, - }; - }); - } - - private void createCalculator() - { - AddStep("create calculator", () => - { - dependencyContainer!.Children = new Drawable[] - { - calculator = new ClicksPerSecondCalculator(), - new DependencyProvidingContainer - { - RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) }, - Child = new ClicksPerSecondCounter // For visual debugging, has no real purpose in the tests - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(5), + (typeof(DrawableRuleset), drawableRuleset), + (typeof(IGameplayClock), gameplayClockContainer) } } - }; - calculator!.Listener = listener = new ManualInputListener(calculator!); + }); }); } @@ -82,6 +65,7 @@ private void createCalculator() public void TestBasicConsistency() { createCalculator(); + startClock(); AddStep("Create gradually increasing KPS inputs", () => { @@ -101,6 +85,7 @@ public void TestBasicConsistency() public void TestRateAdjustConsistency() { createCalculator(); + startClock(); AddStep("Create consistent KPS inputs", () => addInputs(generateConsistentKps(10))); @@ -125,6 +110,7 @@ public void TestRateAdjustConsistency() public void TestInputsDiscardedOnRewind() { createCalculator(); + startClock(); AddStep("Create consistent KPS inputs", () => addInputs(generateConsistentKps(10))); seek(1000); @@ -136,38 +122,79 @@ public void TestInputsDiscardedOnRewind() AddAssert("KPS didn't changed", () => calculator!.Value == 10); } - private void seek(double time) => AddStep($"Seek main clock to {time}ms", () => mainClock?.Seek(time)); + private void seekAllClocks(double time) + { + gameplayClockContainer?.Seek(time); + manualClock!.CurrentTime = time; + } + + protected override Ruleset CreateRuleset() => new OsuRuleset(); - private void changeRate(double rate) => AddStep($"Change rate to x{rate}", () => - (mainClock?.UnderlyingClock as MockFrameBasedClock)!.Rate = rate); + #region Quick steps methods - private void advanceForwards(int frames = 1) => AddStep($"Advance main clock {frames} frame(s) forward.", () => + private void createCalculator() { - if (mainClock == null) return; + AddStep("create calculator", () => + { + Debug.Assert(dependencyContainer?.Dependencies.Get(typeof(DrawableRuleset)) is DrawableRuleset); + dependencyContainer!.Children = new Drawable[] + { + calculator = new ClicksPerSecondCalculator(), + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) }, + Child = new ClicksPerSecondCounter // For visual debugging, has no real purpose in the tests + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5), + } + } + }; + calculator!.Listener = listener = new ManualInputListener(calculator!); + }); + } + + private void seek(double time) => AddStep($"Seek clocks to {time}ms", () => seekAllClocks(time)); - MockFrameBasedClock underlyingClock = (MockFrameBasedClock)mainClock.UnderlyingClock; - underlyingClock.Backwards = false; + private void changeRate(double rate) => AddStep($"Change rate to x{rate}", () => manualClock!.Rate = rate); - for (int i = 0; i < frames; i++) + private void advanceForwards(double time) => + AddStep($"Advance clocks {time} seconds forward.", () => { - underlyingClock.ProcessFrame(); - } + gameplayClockContainer!.Seek(gameplayClockContainer.CurrentTime + time * manualClock!.Rate); + + for (int i = 0; i < time; i++) + { + frameStableClock?.ProcessFrame(); + } + }); + + private void startClock() => AddStep("Start clocks", () => + { + gameplayClockContainer?.Start(); + manualClock!.Rate = 1; }); + #endregion + + #region Input generation + private void addInputs(IEnumerable inputs) { - Debug.Assert(mainClock != null && listener != null); + Debug.Assert(manualClock != null && listener != null && gameplayClockContainer != null); if (!inputs.Any()) return; - double baseTime = mainClock.CurrentTime; + double baseTime = gameplayClockContainer.CurrentTime; foreach (double timestamp in inputs) { - mainClock.Seek(timestamp); + seekAllClocks(timestamp); listener.AddInput(); } - mainClock.Seek(baseTime); + seekAllClocks(baseTime); } private IEnumerable generateGraduallyIncreasingKps() @@ -200,122 +227,111 @@ private IEnumerable generateConsistentKps(double kps, double start = 0, } } - protected override Ruleset CreateRuleset() => new ManiaRuleset(); + #endregion - #region Mock classes + #region Test classes - private class ManualInputListener : ClicksPerSecondCalculator.InputListener + private class TestFrameStableClock : IFrameStableClock { - public void AddInput() => Calculator.AddTimestamp(); - - public ManualInputListener(ClicksPerSecondCalculator calculator) - : base(calculator) + public TestFrameStableClock(IClock source, double startTime = 0) { + this.source = source; + StartTime = startTime; + + if (source is ManualClock manualClock) + { + manualClock.CurrentTime = startTime; + } } - } - private class MockFrameBasedClock : ManualClock, IFrameBasedClock - { - public const double FRAME_INTERVAL = 1000; - public bool Backwards; + public double CurrentTime => source.CurrentTime; + public double Rate => source.Rate; + public bool IsRunning => source.IsRunning; - public MockFrameBasedClock() - { - Rate = 1; - IsRunning = true; - } + private IClock source; public void ProcessFrame() { - CurrentTime += FRAME_INTERVAL * Rate * (Backwards ? -1 : 1); - TimeInfo = new FrameTimeInfo + if (source is ManualClock manualClock) { - Current = CurrentTime, - Elapsed = FRAME_INTERVAL * Rate * (Backwards ? -1 : 1) - }; - } + manualClock.CurrentTime += 1000 * Rate; + } - public void Seek(double time) - { TimeInfo = new FrameTimeInfo { - Elapsed = time - CurrentTime, - Current = CurrentTime = time + Elapsed = 1000 * Rate, + Current = CurrentTime }; } public double ElapsedFrameTime => TimeInfo.Elapsed; - public double FramesPerSecond => 1 / FRAME_INTERVAL; + public double FramesPerSecond => 1 / ElapsedFrameTime * 1000; public FrameTimeInfo TimeInfo { get; private set; } + + public double? StartTime { get; } + public IEnumerable NonGameplayAdjustments => Enumerable.Empty(); + public IBindable IsCatchingUp => new Bindable(); + public IBindable WaitingOnFrames => new Bindable(); } - private class MockFrameStableClock : IGameplayClock, IFrameStableClock + private class ManualInputListener : ClicksPerSecondCalculator.InputListener { - internal readonly IFrameBasedClock UnderlyingClock; - - public readonly BindableBool IsPaused = new BindableBool(); + public void AddInput() => Calculator.AddTimestamp(); - public MockFrameStableClock(MockFrameBasedClock underlyingClock) + public ManualInputListener(ClicksPerSecondCalculator calculator) + : base(calculator) { - UnderlyingClock = underlyingClock; } - - public void Seek(double time) => (UnderlyingClock as MockFrameBasedClock)?.Seek(time); - - public IBindable IsCatchingUp => new Bindable(); - public IBindable WaitingOnFrames => new Bindable(); - public double CurrentTime => UnderlyingClock.CurrentTime; - public double Rate => UnderlyingClock.Rate; - public bool IsRunning => UnderlyingClock.IsRunning; - public void ProcessFrame() => UnderlyingClock.ProcessFrame(); - - public double ElapsedFrameTime => UnderlyingClock.ElapsedFrameTime; - public double FramesPerSecond => UnderlyingClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => UnderlyingClock.TimeInfo; - public double TrueGameplayRate => UnderlyingClock.Rate; - public double? StartTime => 0; - public IEnumerable NonGameplayAdjustments => Enumerable.Empty(); - IBindable IGameplayClock.IsPaused => IsPaused; } - private class MockDrawableRuleset : DrawableRuleset +#nullable disable + + [SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")] + private class TestDrawableRuleset : DrawableRuleset { - public MockDrawableRuleset(Ruleset ruleset, IFrameStableClock clock) - : base(ruleset) + public override IEnumerable Objects => Enumerable.Empty(); + + public override event Action NewResult + { + add => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context"); + remove => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context"); + } + + public override event Action RevertResult { - FrameStableClock = clock; + add => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); + remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); } -#pragma warning disable CS0067 - public override event Action? NewResult; - public override event Action? RevertResult; -#pragma warning restore CS0067 - public override Playfield? Playfield => null; - public override Container? Overlays => null; - public override Container? FrameStableComponents => null; + public override Playfield Playfield => null; + public override Container Overlays => null; + public override Container FrameStableComponents => null; public override IFrameStableClock FrameStableClock { get; } internal override bool FrameStablePlayback { get; set; } public override IReadOnlyList Mods => Array.Empty(); - public override IEnumerable Objects => Array.Empty(); + public override double GameplayStartTime => 0; - public override GameplayCursorContainer? Cursor => null; + public override GameplayCursorContainer Cursor => null; - public override void SetReplayScore(Score replayScore) + public TestDrawableRuleset() + : base(new OsuRuleset()) { } - public override void SetRecordTarget(Score score) + public TestDrawableRuleset(IFrameStableClock frameStableClock) + : this() { + FrameStableClock = frameStableClock; } - public override void RequestResume(Action continueResume) - { - } + public override void SetReplayScore(Score replayScore) => throw new NotImplementedException(); - public override void CancelResume() - { - } + public override void SetRecordTarget(Score score) => throw new NotImplementedException(); + + public override void RequestResume(Action continueResume) => throw new NotImplementedException(); + + public override void CancelResume() => throw new NotImplementedException(); } #endregion From b5970495242d9555fad30c52a33b1e66f3f3a4e9 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Mon, 22 Aug 2022 10:47:37 +0200 Subject: [PATCH 29/38] Code cleanup for CPS tests - Remove null-forgiving operator usages - Fix code quality issues mentionned by NVika --- .../Gameplay/TestSceneClicksPerSecond.cs | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs index 375726dd9a47..137ab7acdbc7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs @@ -27,11 +27,11 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneClicksPerSecond : OsuTestScene { - private DependencyProvidingContainer? dependencyContainer; - private ClicksPerSecondCalculator? calculator; + private DependencyProvidingContainer dependencyContainer = null!; + private ClicksPerSecondCalculator calculator = null!; private ManualInputListener? listener; - private GameplayClockContainer? gameplayClockContainer; - private ManualClock? manualClock; + private GameplayClockContainer gameplayClockContainer = null!; + private ManualClock manualClock = null!; private DrawableRuleset? drawableRuleset; private IFrameStableClock? frameStableClock; @@ -77,7 +77,7 @@ public void TestBasicConsistency() seek(i * 10000); advanceForwards(2); int kps = i + 1; - AddAssert($"{kps} KPS", () => calculator!.Value == kps); + AddAssert($"{kps} KPS", () => calculator.Value == kps); } } @@ -95,14 +95,14 @@ public void TestRateAdjustConsistency() { changeRate(i); double rate = i; - AddAssert($"KPS approx. = {i}", () => MathHelper.ApproximatelyEquivalent(calculator!.Value, 10 * rate, 0.5)); + AddAssert($"KPS approx. = {i}", () => MathHelper.ApproximatelyEquivalent(calculator.Value, 10 * rate, 0.5)); } for (double i = 1; i >= 0.5; i -= 0.25) { changeRate(i); double rate = i; - AddAssert($"KPS approx. = {i}", () => MathHelper.ApproximatelyEquivalent(calculator!.Value, 10 * rate, 0.5)); + AddAssert($"KPS approx. = {i}", () => MathHelper.ApproximatelyEquivalent(calculator.Value, 10 * rate, 0.5)); } } @@ -115,17 +115,17 @@ public void TestInputsDiscardedOnRewind() AddStep("Create consistent KPS inputs", () => addInputs(generateConsistentKps(10))); seek(1000); - AddAssert("KPS = 10", () => calculator!.Value == 10); + AddAssert("KPS = 10", () => calculator.Value == 10); AddStep("Create delayed inputs", () => addInputs(generateConsistentKps(10, 50))); seek(1000); - AddAssert("KPS didn't changed", () => calculator!.Value == 10); + AddAssert("KPS didn't changed", () => calculator.Value == 10); } private void seekAllClocks(double time) { - gameplayClockContainer?.Seek(time); - manualClock!.CurrentTime = time; + gameplayClockContainer.Seek(time); + manualClock.CurrentTime = time; } protected override Ruleset CreateRuleset() => new OsuRuleset(); @@ -136,8 +136,7 @@ private void createCalculator() { AddStep("create calculator", () => { - Debug.Assert(dependencyContainer?.Dependencies.Get(typeof(DrawableRuleset)) is DrawableRuleset); - dependencyContainer!.Children = new Drawable[] + dependencyContainer.Children = new Drawable[] { calculator = new ClicksPerSecondCalculator(), new DependencyProvidingContainer @@ -152,18 +151,18 @@ private void createCalculator() } } }; - calculator!.Listener = listener = new ManualInputListener(calculator!); + calculator.Listener = listener = new ManualInputListener(calculator); }); } private void seek(double time) => AddStep($"Seek clocks to {time}ms", () => seekAllClocks(time)); - private void changeRate(double rate) => AddStep($"Change rate to x{rate}", () => manualClock!.Rate = rate); + private void changeRate(double rate) => AddStep($"Change rate to x{rate}", () => manualClock.Rate = rate); private void advanceForwards(double time) => AddStep($"Advance clocks {time} seconds forward.", () => { - gameplayClockContainer!.Seek(gameplayClockContainer.CurrentTime + time * manualClock!.Rate); + gameplayClockContainer.Seek(gameplayClockContainer.CurrentTime + time * manualClock.Rate); for (int i = 0; i < time; i++) { @@ -173,8 +172,8 @@ private void advanceForwards(double time) => private void startClock() => AddStep("Start clocks", () => { - gameplayClockContainer?.Start(); - manualClock!.Rate = 1; + gameplayClockContainer.Start(); + manualClock.Rate = 1; }); #endregion @@ -183,7 +182,6 @@ private void startClock() => AddStep("Start clocks", () => private void addInputs(IEnumerable inputs) { - Debug.Assert(manualClock != null && listener != null && gameplayClockContainer != null); if (!inputs.Any()) return; double baseTime = gameplayClockContainer.CurrentTime; @@ -191,7 +189,7 @@ private void addInputs(IEnumerable inputs) foreach (double timestamp in inputs) { seekAllClocks(timestamp); - listener.AddInput(); + listener?.AddInput(); } seekAllClocks(baseTime); @@ -199,7 +197,7 @@ private void addInputs(IEnumerable inputs) private IEnumerable generateGraduallyIncreasingKps() { - IEnumerable? final = null; + IEnumerable final = null!; for (int i = 1; i <= 10; i++) { @@ -211,10 +209,10 @@ private IEnumerable generateGraduallyIncreasingKps() continue; } - final = final!.Concat(currentKps); + final = final.Concat(currentKps); } - return final!; + return final; } private IEnumerable generateConsistentKps(double kps, double start = 0, double duration = 10) @@ -236,7 +234,6 @@ private class TestFrameStableClock : IFrameStableClock public TestFrameStableClock(IClock source, double startTime = 0) { this.source = source; - StartTime = startTime; if (source is ManualClock manualClock) { @@ -248,7 +245,7 @@ public TestFrameStableClock(IClock source, double startTime = 0) public double Rate => source.Rate; public bool IsRunning => source.IsRunning; - private IClock source; + private readonly IClock source; public void ProcessFrame() { @@ -268,7 +265,6 @@ public void ProcessFrame() public double FramesPerSecond => 1 / ElapsedFrameTime * 1000; public FrameTimeInfo TimeInfo { get; private set; } - public double? StartTime { get; } public IEnumerable NonGameplayAdjustments => Enumerable.Empty(); public IBindable IsCatchingUp => new Bindable(); public IBindable WaitingOnFrames => new Bindable(); From f3847b90fddaa6418c457a68ddd2912f637b0443 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Aug 2022 19:36:01 +0900 Subject: [PATCH 30/38] Tidy up attach logic --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 7 ++++--- osu.Game/Rulesets/UI/RulesetInputManager.cs | 23 +++++++++------------ osu.Game/Screens/Play/HUDOverlay.cs | 7 +++++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 02df4d8fb33e..73acb1759fe7 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.UI /// Displays an interactive ruleset gameplay instance. /// /// The type of HitObject contained by this DrawableRuleset. - public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter + public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachHUDPieces where TObject : HitObject { public override event Action NewResult; @@ -339,9 +339,10 @@ public override void SetReplayScore(Score replayScore) public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); public void Attach(KeyCounterDisplay keyCounter) => - (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(keyCounter); - public void Attach(ClicksPerSecondCalculator calculator) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(calculator); + public void Attach(ClicksPerSecondCalculator calculator) => + (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(calculator); /// /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 4b7ce22cfccd..401ebbfd740d 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.UI { - public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler + public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachHUDPieces, IHasReplayHandler, IHasRecordingHandler where T : struct { public readonly KeyBindingContainer KeyBindingContainer; @@ -169,7 +169,7 @@ public void Attach(KeyCounterDisplay keyCounter) .Select(action => new KeyCounterAction(action))); } - public class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler + private class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler { public ActionReceptor(KeyCounterDisplay target) : base(target) @@ -191,8 +191,6 @@ public void OnReleased(KeyBindingReleaseEvent e) public void Attach(ClicksPerSecondCalculator calculator) { - if (calculator == null) return; - var listener = new ActionListener(calculator); KeyBindingContainer.Add(listener); @@ -200,23 +198,22 @@ public void Attach(ClicksPerSecondCalculator calculator) calculator.Listener = listener; } - public class ActionListener : ClicksPerSecondCalculator.InputListener, IKeyBindingHandler + private class ActionListener : ClicksPerSecondCalculator.InputListener, IKeyBindingHandler { + public ActionListener(ClicksPerSecondCalculator calculator) + : base(calculator) + { + } + public bool OnPressed(KeyBindingPressEvent e) { Calculator.AddTimestamp(); - return false; } public void OnReleased(KeyBindingReleaseEvent e) { } - - public ActionListener(ClicksPerSecondCalculator calculator) - : base(calculator) - { - } } #endregion @@ -256,10 +253,10 @@ public interface IHasRecordingHandler } /// - /// Supports attaching a . + /// Supports attaching various HUD pieces. /// Keys will be populated automatically and a receptor will be injected inside. /// - public interface ICanAttachKeyCounter + public interface ICanAttachHUDPieces { void Attach(KeyCounterDisplay keyCounter); void Attach(ClicksPerSecondCalculator calculator); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index b27efaf13a13..f9f3693385f1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -264,8 +264,11 @@ private void replayLoadedValueChanged(ValueChangedEvent e) protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) { - (drawableRuleset as ICanAttachKeyCounter)?.Attach(KeyCounter); - (drawableRuleset as ICanAttachKeyCounter)?.Attach(clicksPerSecondCalculator); + if (drawableRuleset is ICanAttachHUDPieces attachTarget) + { + attachTarget.Attach(KeyCounter); + attachTarget.Attach(clicksPerSecondCalculator); + } replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); } From 5cddc7ed1f8fadccedccc4b6efe0a836d1ec3037 Mon Sep 17 00:00:00 2001 From: Ryuki Date: Wed, 24 Aug 2022 17:12:52 +0200 Subject: [PATCH 31/38] Code cleanup (CPS) --- .../Gameplay/TestSceneClicksPerSecond.cs | 31 ++----- osu.Game/Rulesets/UI/RulesetInputManager.cs | 10 +-- .../ClicksPerSecondCalculator.cs | 90 ++++++------------- .../ClicksPerSecond/ClicksPerSecondCounter.cs | 6 +- 4 files changed, 39 insertions(+), 98 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs index 137ab7acdbc7..a14046025194 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs @@ -29,7 +29,6 @@ public class TestSceneClicksPerSecond : OsuTestScene { private DependencyProvidingContainer dependencyContainer = null!; private ClicksPerSecondCalculator calculator = null!; - private ManualInputListener? listener; private GameplayClockContainer gameplayClockContainer = null!; private ManualClock manualClock = null!; private DrawableRuleset? drawableRuleset; @@ -151,7 +150,6 @@ private void createCalculator() } } }; - calculator.Listener = listener = new ManualInputListener(calculator); }); } @@ -189,7 +187,7 @@ private void addInputs(IEnumerable inputs) foreach (double timestamp in inputs) { seekAllClocks(timestamp); - listener?.AddInput(); + calculator.AddTimestamp(); } seekAllClocks(baseTime); @@ -270,18 +268,6 @@ public void ProcessFrame() public IBindable WaitingOnFrames => new Bindable(); } - private class ManualInputListener : ClicksPerSecondCalculator.InputListener - { - public void AddInput() => Calculator.AddTimestamp(); - - public ManualInputListener(ClicksPerSecondCalculator calculator) - : base(calculator) - { - } - } - -#nullable disable - [SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")] private class TestDrawableRuleset : DrawableRuleset { @@ -299,24 +285,19 @@ public override event Action RevertResult remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); } - public override Playfield Playfield => null; - public override Container Overlays => null; - public override Container FrameStableComponents => null; + public override Playfield Playfield => null!; + public override Container Overlays => null!; + public override Container FrameStableComponents => null!; public override IFrameStableClock FrameStableClock { get; } internal override bool FrameStablePlayback { get; set; } public override IReadOnlyList Mods => Array.Empty(); public override double GameplayStartTime => 0; - public override GameplayCursorContainer Cursor => null; - - public TestDrawableRuleset() - : base(new OsuRuleset()) - { - } + public override GameplayCursorContainer Cursor => null!; public TestDrawableRuleset(IFrameStableClock frameStableClock) - : this() + : base(new OsuRuleset()) { FrameStableClock = frameStableClock; } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 401ebbfd740d..dcd2c4fb5d47 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -194,20 +194,20 @@ public void Attach(ClicksPerSecondCalculator calculator) var listener = new ActionListener(calculator); KeyBindingContainer.Add(listener); - - calculator.Listener = listener; } - private class ActionListener : ClicksPerSecondCalculator.InputListener, IKeyBindingHandler + private class ActionListener : Component, IKeyBindingHandler { + private readonly ClicksPerSecondCalculator calculator; + public ActionListener(ClicksPerSecondCalculator calculator) - : base(calculator) { + this.calculator = calculator; } public bool OnPressed(KeyBindingPressEvent e) { - Calculator.AddTimestamp(); + calculator.AddTimestamp(); return false; } diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index d9c3c6ffec2e..20ac923b8220 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Timing; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD.ClicksPerSecond @@ -15,89 +13,53 @@ public class ClicksPerSecondCalculator : Component { private readonly List timestamps; - private InputListener? listener; - [Resolved] - private IGameplayClock? gameplayClock { get; set; } - - [Resolved(canBeNull: true)] - private DrawableRuleset? drawableRuleset { get; set; } - - public InputListener Listener - { - set - { - onResetRequested?.Invoke(); - listener = value; - } - } + private IGameplayClock gameplayClock { get; set; } = null!; - private event Action? onResetRequested; - - private IClock? workingClock => drawableRuleset?.FrameStableClock; + [Resolved] + private DrawableRuleset drawableRuleset { get; set; } = null!; - private double baseRate; + private double rate; - private double rate - { - get - { - if (gameplayClock?.TrueGameplayRate > 0) - { - baseRate = gameplayClock.TrueGameplayRate; - } + // The latest timestamp GC seeked. Does not affect normal gameplay + // but prevents duplicate inputs on replays. + private double latestTime = double.NegativeInfinity; - return baseRate; - } - } - - private double maxTime = double.NegativeInfinity; - - public bool Ready => workingClock != null && gameplayClock != null && listener != null; - public int Value => timestamps.Count(isTimestampWithinSpan); + public int Value { get; private set; } public ClicksPerSecondCalculator() { RelativeSizeAxes = Axes.Both; timestamps = new List(); - onResetRequested += cleanUp; - } - - private void cleanUp() - { - timestamps.Clear(); - maxTime = double.NegativeInfinity; } - public void AddTimestamp() + protected override void Update() { - if (workingClock == null) return; + base.Update(); - if (workingClock.CurrentTime >= maxTime) + // When pausing in replays (using the space bar) GC.TrueGameplayRate returns 0 + // To prevent CPS value being 0, we store and use the last non-zero TrueGameplayRate + if (gameplayClock.TrueGameplayRate > 0) { - timestamps.Add(workingClock.CurrentTime); - maxTime = workingClock.CurrentTime; + rate = gameplayClock.TrueGameplayRate; } - } - - private bool isTimestampWithinSpan(double timestamp) - { - if (workingClock == null) return false; - double span = 1000 * rate; - double relativeTime = workingClock.CurrentTime - timestamp; - return relativeTime > 0 && relativeTime <= span; + Value = timestamps.Count(timestamp => + { + double window = 1000 * rate; + double relativeTime = drawableRuleset.FrameStableClock.CurrentTime - timestamp; + return relativeTime > 0 && relativeTime <= window; + }); } - public abstract class InputListener : Component + public void AddTimestamp() { - protected ClicksPerSecondCalculator Calculator; - - protected InputListener(ClicksPerSecondCalculator calculator) + // Discard inputs if current gameplay time is not the latest + // to prevent duplicate inputs + if (drawableRuleset.FrameStableClock.CurrentTime >= latestTime) { - RelativeSizeAxes = Axes.Both; - Depth = float.MinValue; - Calculator = calculator; + timestamps.Add(drawableRuleset.FrameStableClock.CurrentTime); + latestTime = drawableRuleset.FrameStableClock.CurrentTime; } } } diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs index 3ff06d52171e..243d8ed1e86e 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCounter.cs @@ -16,9 +16,7 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { public class ClicksPerSecondCounter : RollingCounter, ISkinnableDrawable { - private const float alpha_when_invalid = 0.3f; - - [Resolved(canBeNull: false)] + [Resolved] private ClicksPerSecondCalculator calculator { get; set; } = null!; protected override double RollingDuration => 350; @@ -40,7 +38,7 @@ protected override void Update() { base.Update(); - Current.Value = calculator.Ready ? calculator.Value : 0; + Current.Value = calculator.Value; } protected override IHasText CreateText() => new TextComponent(); From 5c2fb3e43453febce253e83c74f5e78e0659bdcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Sep 2022 18:22:53 +0900 Subject: [PATCH 32/38] Simplify calculation method --- .../ClicksPerSecondCalculator.cs | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index 20ac923b8220..b8cf20ee8f5f 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.UI; @@ -11,56 +10,48 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond { public class ClicksPerSecondCalculator : Component { - private readonly List timestamps; + private readonly List timestamps = new List(); [Resolved] private IGameplayClock gameplayClock { get; set; } = null!; - [Resolved] - private DrawableRuleset drawableRuleset { get; set; } = null!; - - private double rate; - - // The latest timestamp GC seeked. Does not affect normal gameplay - // but prevents duplicate inputs on replays. - private double latestTime = double.NegativeInfinity; + [Resolved(canBeNull: true)] + private DrawableRuleset? drawableRuleset { get; set; } public int Value { get; private set; } + private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock; + public ClicksPerSecondCalculator() { RelativeSizeAxes = Axes.Both; - timestamps = new List(); } + public void AddInputTimestamp() => timestamps.Add(clock.CurrentTime); + protected override void Update() { base.Update(); - // When pausing in replays (using the space bar) GC.TrueGameplayRate returns 0 - // To prevent CPS value being 0, we store and use the last non-zero TrueGameplayRate - if (gameplayClock.TrueGameplayRate > 0) - { - rate = gameplayClock.TrueGameplayRate; - } + double latestValidTime = clock.CurrentTime; + double earliestTimeValid = latestValidTime - 1000 * gameplayClock.TrueGameplayRate; - Value = timestamps.Count(timestamp => - { - double window = 1000 * rate; - double relativeTime = drawableRuleset.FrameStableClock.CurrentTime - timestamp; - return relativeTime > 0 && relativeTime <= window; - }); - } + int count = 0; - public void AddTimestamp() - { - // Discard inputs if current gameplay time is not the latest - // to prevent duplicate inputs - if (drawableRuleset.FrameStableClock.CurrentTime >= latestTime) + for (int i = timestamps.Count - 1; i >= 0; i--) { - timestamps.Add(drawableRuleset.FrameStableClock.CurrentTime); - latestTime = drawableRuleset.FrameStableClock.CurrentTime; + // handle rewinding by removing future timestamps as we go + if (timestamps[i] > latestValidTime) + { + timestamps.RemoveAt(i); + continue; + } + + if (timestamps[i] >= earliestTimeValid) + count++; } + + Value = count; } } } From 6729bb3e1a24e38c92caa542c3e67ddc1c59ad47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Sep 2022 18:23:54 +0900 Subject: [PATCH 33/38] Change `FrameStableClock` to inherit `IGameplayClock` --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- osu.Game/Rulesets/UI/IFrameStableClock.cs | 4 ++-- osu.Game/Rulesets/UI/RulesetInputManager.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 18d0ff0bedd2..9446ba946b05 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.UI /// [Cached(typeof(IGameplayClock))] [Cached(typeof(IFrameStableClock))] - public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock + public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock { public ReplayInputHandler? ReplayInputHandler { get; set; } diff --git a/osu.Game/Rulesets/UI/IFrameStableClock.cs b/osu.Game/Rulesets/UI/IFrameStableClock.cs index 569ef5e06cc0..4e50d059e9e6 100644 --- a/osu.Game/Rulesets/UI/IFrameStableClock.cs +++ b/osu.Game/Rulesets/UI/IFrameStableClock.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Timing; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.UI { - public interface IFrameStableClock : IFrameBasedClock + public interface IFrameStableClock : IGameplayClock { IBindable IsCatchingUp { get; } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index dcd2c4fb5d47..1a97153f2f91 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -207,7 +207,7 @@ public ActionListener(ClicksPerSecondCalculator calculator) public bool OnPressed(KeyBindingPressEvent e) { - calculator.AddTimestamp(); + calculator.AddInputTimestamp(); return false; } From ee094e3a858e409f21b20602438595d8a770d372 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Sep 2022 19:05:13 +0900 Subject: [PATCH 34/38] Rewrite tests --- .../Gameplay/TestSceneClicksPerSecond.cs | 292 +++--------------- 1 file changed, 50 insertions(+), 242 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs index a14046025194..ed5102e3b3ff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs @@ -3,22 +3,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; 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.Judgements; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.UI; -using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osuTK; @@ -27,290 +16,109 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneClicksPerSecond : OsuTestScene { - private DependencyProvidingContainer dependencyContainer = null!; private ClicksPerSecondCalculator calculator = null!; - private GameplayClockContainer gameplayClockContainer = null!; - private ManualClock manualClock = null!; - private DrawableRuleset? drawableRuleset; - private IFrameStableClock? frameStableClock; + + private TestGameplayClock manualGameplayClock = null!; [SetUpSteps] public void SetUpSteps() { AddStep("create components", () => { - var ruleset = CreateRuleset(); - - Debug.Assert(ruleset != null); + manualGameplayClock = new TestGameplayClock(); - Child = gameplayClockContainer = new GameplayClockContainer(manualClock = new ManualClock()); - gameplayClockContainer.AddRange(new Drawable[] + Child = new DependencyProvidingContainer { - drawableRuleset = new TestDrawableRuleset(frameStableClock = new TestFrameStableClock(manualClock)), - dependencyContainer = new DependencyProvidingContainer + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(IGameplayClock), manualGameplayClock) }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] + calculator = new ClicksPerSecondCalculator(), + new DependencyProvidingContainer { - (typeof(DrawableRuleset), drawableRuleset), - (typeof(IGameplayClock), gameplayClockContainer) + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) }, + Child = new ClicksPerSecondCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5), + } } - } - }); + }, + }; }); } [Test] public void TestBasicConsistency() { - createCalculator(); - startClock(); - - AddStep("Create gradually increasing KPS inputs", () => - { - addInputs(generateGraduallyIncreasingKps()); - }); - - for (int i = 0; i < 10; i++) - { - seek(i * 10000); - advanceForwards(2); - int kps = i + 1; - AddAssert($"{kps} KPS", () => calculator.Value == kps); - } + seek(1000); + AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 })); + checkClicksPerSecondValue(10); } [Test] public void TestRateAdjustConsistency() { - createCalculator(); - startClock(); - - AddStep("Create consistent KPS inputs", () => addInputs(generateConsistentKps(10))); - - advanceForwards(2); - - for (double i = 1; i <= 2; i += 0.25) - { - changeRate(i); - double rate = i; - AddAssert($"KPS approx. = {i}", () => MathHelper.ApproximatelyEquivalent(calculator.Value, 10 * rate, 0.5)); - } - - for (double i = 1; i >= 0.5; i -= 0.25) - { - changeRate(i); - double rate = i; - AddAssert($"KPS approx. = {i}", () => MathHelper.ApproximatelyEquivalent(calculator.Value, 10 * rate, 0.5)); - } + seek(1000); + AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 })); + checkClicksPerSecondValue(10); + AddStep("set rate 0.5x", () => manualGameplayClock.TrueGameplayRate = 0.5); + checkClicksPerSecondValue(5); } [Test] public void TestInputsDiscardedOnRewind() { - createCalculator(); - startClock(); - - AddStep("Create consistent KPS inputs", () => addInputs(generateConsistentKps(10))); seek(1000); - - AddAssert("KPS = 10", () => calculator.Value == 10); - - AddStep("Create delayed inputs", () => addInputs(generateConsistentKps(10, 50))); + AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 })); + checkClicksPerSecondValue(10); + seek(500); + checkClicksPerSecondValue(6); seek(1000); - AddAssert("KPS didn't changed", () => calculator.Value == 10); - } - - private void seekAllClocks(double time) - { - gameplayClockContainer.Seek(time); - manualClock.CurrentTime = time; + checkClicksPerSecondValue(6); } - protected override Ruleset CreateRuleset() => new OsuRuleset(); - - #region Quick steps methods + private void checkClicksPerSecondValue(int i) => AddAssert("clicks/s is correct", () => calculator.Value, () => Is.EqualTo(i)); - private void createCalculator() - { - AddStep("create calculator", () => - { - dependencyContainer.Children = new Drawable[] - { - calculator = new ClicksPerSecondCalculator(), - new DependencyProvidingContainer - { - RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) }, - Child = new ClicksPerSecondCounter // For visual debugging, has no real purpose in the tests - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(5), - } - } - }; - }); - } + private void seekClockImmediately(double time) => manualGameplayClock.CurrentTime = time; - private void seek(double time) => AddStep($"Seek clocks to {time}ms", () => seekAllClocks(time)); - - private void changeRate(double rate) => AddStep($"Change rate to x{rate}", () => manualClock.Rate = rate); - - private void advanceForwards(double time) => - AddStep($"Advance clocks {time} seconds forward.", () => - { - gameplayClockContainer.Seek(gameplayClockContainer.CurrentTime + time * manualClock.Rate); - - for (int i = 0; i < time; i++) - { - frameStableClock?.ProcessFrame(); - } - }); - - private void startClock() => AddStep("Start clocks", () => - { - gameplayClockContainer.Start(); - manualClock.Rate = 1; - }); - - #endregion - - #region Input generation + private void seek(double time) => AddStep($"Seek to {time}ms", () => seekClockImmediately(time)); private void addInputs(IEnumerable inputs) { - if (!inputs.Any()) return; - - double baseTime = gameplayClockContainer.CurrentTime; + double baseTime = manualGameplayClock.CurrentTime; foreach (double timestamp in inputs) { - seekAllClocks(timestamp); - calculator.AddTimestamp(); + seekClockImmediately(timestamp); + calculator.AddInputTimestamp(); } - seekAllClocks(baseTime); + seekClockImmediately(baseTime); } - private IEnumerable generateGraduallyIncreasingKps() + private class TestGameplayClock : IGameplayClock { - IEnumerable final = null!; + public double CurrentTime { get; set; } - for (int i = 1; i <= 10; i++) - { - var currentKps = generateConsistentKps(i, (i - 1) * 10000); - - if (i == 1) - { - final = currentKps; - continue; - } - - final = final.Concat(currentKps); - } - - return final; - } - - private IEnumerable generateConsistentKps(double kps, double start = 0, double duration = 10) - { - double end = start + 1000 * duration; + public double Rate => 1; - for (; start < end; start += 1000 / kps) - { - yield return start; - } - } + public bool IsRunning => true; - #endregion - - #region Test classes - - private class TestFrameStableClock : IFrameStableClock - { - public TestFrameStableClock(IClock source, double startTime = 0) - { - this.source = source; - - if (source is ManualClock manualClock) - { - manualClock.CurrentTime = startTime; - } - } - - public double CurrentTime => source.CurrentTime; - public double Rate => source.Rate; - public bool IsRunning => source.IsRunning; - - private readonly IClock source; + public double TrueGameplayRate { get; set; } = 1; public void ProcessFrame() { - if (source is ManualClock manualClock) - { - manualClock.CurrentTime += 1000 * Rate; - } - - TimeInfo = new FrameTimeInfo - { - Elapsed = 1000 * Rate, - Current = CurrentTime - }; - } - - public double ElapsedFrameTime => TimeInfo.Elapsed; - public double FramesPerSecond => 1 / ElapsedFrameTime * 1000; - public FrameTimeInfo TimeInfo { get; private set; } - - public IEnumerable NonGameplayAdjustments => Enumerable.Empty(); - public IBindable IsCatchingUp => new Bindable(); - public IBindable WaitingOnFrames => new Bindable(); - } - - [SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")] - private class TestDrawableRuleset : DrawableRuleset - { - public override IEnumerable Objects => Enumerable.Empty(); - - public override event Action NewResult - { - add => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context"); - remove => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context"); } - public override event Action RevertResult - { - add => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); - remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); - } - - public override Playfield Playfield => null!; - public override Container Overlays => null!; - public override Container FrameStableComponents => null!; - public override IFrameStableClock FrameStableClock { get; } - - internal override bool FrameStablePlayback { get; set; } - public override IReadOnlyList Mods => Array.Empty(); - - public override double GameplayStartTime => 0; - public override GameplayCursorContainer Cursor => null!; - - public TestDrawableRuleset(IFrameStableClock frameStableClock) - : base(new OsuRuleset()) - { - FrameStableClock = frameStableClock; - } - - public override void SetReplayScore(Score replayScore) => throw new NotImplementedException(); - - public override void SetRecordTarget(Score score) => throw new NotImplementedException(); - - public override void RequestResume(Action continueResume) => throw new NotImplementedException(); - - public override void CancelResume() => throw new NotImplementedException(); + public double ElapsedFrameTime => throw new NotImplementedException(); + public double FramesPerSecond => throw new NotImplementedException(); + public FrameTimeInfo TimeInfo => throw new NotImplementedException(); + public double StartTime => throw new NotImplementedException(); + public IEnumerable NonGameplayAdjustments => throw new NotImplementedException(); + public IBindable IsPaused => throw new NotImplementedException(); } - - #endregion } } From a98c6b2c1f43e910045cc75a879452a732268be5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Sep 2022 19:14:23 +0900 Subject: [PATCH 35/38] Add comment metioning why we need to use `DrawableRuleset` lookup --- .../Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index b8cf20ee8f5f..82bf9cfa1ec9 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -20,6 +20,8 @@ public class ClicksPerSecondCalculator : Component public int Value { get; private set; } + // Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset` + // as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency. private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock; public ClicksPerSecondCalculator() From 15a4eb46c4d3ea9cf387116837733fc741bd53bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Sep 2022 19:20:26 +0900 Subject: [PATCH 36/38] Rename test scene to match class name --- ...ClicksPerSecond.cs => TestSceneClicksPerSecondCalculator.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Gameplay/{TestSceneClicksPerSecond.cs => TestSceneClicksPerSecondCalculator.cs} (98%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs similarity index 98% rename from osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index ed5102e3b3ff..b740c06e03c7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecond.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneClicksPerSecond : OsuTestScene + public class TestSceneClicksPerSecondCalculator : OsuTestScene { private ClicksPerSecondCalculator calculator = null!; From 45239fc737423f05f63c93b05e72ef20a33ef267 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Sep 2022 23:03:15 +0900 Subject: [PATCH 37/38] Update `TrueGameplayRate` accessing --- .../Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs index 82bf9cfa1ec9..04774b974f05 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondCalculator.cs @@ -36,7 +36,7 @@ protected override void Update() base.Update(); double latestValidTime = clock.CurrentTime; - double earliestTimeValid = latestValidTime - 1000 * gameplayClock.TrueGameplayRate; + double earliestTimeValid = latestValidTime - 1000 * gameplayClock.GetTrueGameplayRate(); int count = 0; From 27aa3552dc03846cf7d23eb7b731b574be20e147 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Sep 2022 00:00:08 +0900 Subject: [PATCH 38/38] Update in line with `TrueGameplayRate` changes --- .../Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index b740c06e03c7..2dad5e2c3241 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using NUnit.Framework; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -107,7 +108,9 @@ private class TestGameplayClock : IGameplayClock public bool IsRunning => true; - public double TrueGameplayRate { get; set; } = 1; + public double TrueGameplayRate { set => adjustableAudioComponent.Tempo.Value = value; } + + private readonly AudioAdjustments adjustableAudioComponent = new AudioAdjustments(); public void ProcessFrame() { @@ -117,6 +120,9 @@ public void ProcessFrame() public double FramesPerSecond => throw new NotImplementedException(); public FrameTimeInfo TimeInfo => throw new NotImplementedException(); public double StartTime => throw new NotImplementedException(); + + public IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent; + public IEnumerable NonGameplayAdjustments => throw new NotImplementedException(); public IBindable IsPaused => throw new NotImplementedException(); }