From c9b205afeb64f721e228ee51adb8eabc0cb93327 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 1 Mar 2022 21:12:06 +0800 Subject: [PATCH 01/21] Add adaptive speed mod --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 1 + osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 1 + osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 122 +++++++++++++++++++++ osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 859b6cfe76..f139a88f50 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -258,6 +258,7 @@ namespace osu.Game.Rulesets.Mania { new MultiMod(new ModWindUp(), new ModWindDown()), new ManiaModMuted(), + new ModAdaptiveSpeed() }; default: diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 5ade164566..5b936b1bf1 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -195,6 +195,7 @@ namespace osu.Game.Rulesets.Osu new OsuModMuted(), new OsuModNoScope(), new OsuModAimAssist(), + new ModAdaptiveSpeed() }; case ModType.System: diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index de0ef8d95b..dc90845d92 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -151,6 +151,7 @@ namespace osu.Game.Rulesets.Taiko { new MultiMod(new ModWindUp(), new ModWindDown()), new TaikoModMuted(), + new ModAdaptiveSpeed() }; default: diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs new file mode 100644 index 0000000000..1c6fbe88cc --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -0,0 +1,122 @@ +// 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.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Audio; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mods +{ + public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap + { + private const double fastest_rate = 2f; + + private const double slowest_rate = 0.5f; + + /// + /// Adjust track rate using the average speed of the last x hits + /// + private const int average_count = 10; + + public override string Name => "Adaptive Speed"; + + public override string Acronym => "AS"; + + public override string Description => "Let track speed adapt to you."; + + public override ModType Type => ModType.Fun; + + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; + + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] + public BindableBool AdjustPitch { get; } = new BindableBool + { + Default = true, + Value = true + }; + + public BindableNumber SpeedChange { get; } = new BindableDouble + { + Default = 1, + Value = 1, + Precision = 0.01, + }; + + private ITrack track; + + private readonly List recentRates = Enumerable.Range(0, average_count).Select(x => 1d).ToList(); + + // rates are calculated using the end time of the previous hit object + // caching them here for easy access + private readonly Dictionary previousEndTimes = new Dictionary(); + + public ModAdaptiveSpeed() + { + AdjustPitch.BindValueChanged(applyPitchAdjustment); + } + + public void ApplyToTrack(ITrack track) + { + this.track = track; + + AdjustPitch.TriggerChange(); + } + + public void ApplyToSample(DrawableSample sample) + { + sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); + } + + public double ApplyToRate(double time, double rate = 1) => rate; + + private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) + { + // remove existing old adjustment + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); + + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + } + + private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) + => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; + + public void ApplyToDrawableHitObject(DrawableHitObject drawable) + { + drawable.OnNewResult += (o, result) => + { + if (!result.IsHit) return; + if (!previousEndTimes.ContainsKey(result.HitObject)) return; + + double prevEndTime = previousEndTimes[result.HitObject]; + + recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, slowest_rate, fastest_rate)); + if (recentRates.Count > average_count) + recentRates.RemoveAt(0); + + SpeedChange.Value = recentRates.Average(); + }; + } + + public void ApplyToBeatmap(IBeatmap beatmap) + { + for (int i = 1; i < beatmap.HitObjects.Count; i++) + { + var hitObject = beatmap.HitObjects[i]; + var previousObject = beatmap.HitObjects.Take(i).LastOrDefault(o => !Precision.AlmostBigger(o.GetEndTime(), hitObject.GetEndTime())); + + if (previousObject != null) + previousEndTimes.Add(hitObject, previousObject.GetEndTime()); + } + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index e66650f7b4..ebe18f2188 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mods public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value; - public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) }; + public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp), typeof(ModAdaptiveSpeed) }; public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index b5cd64dafa..b6b2decede 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } - public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust) }; + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; From 783f43ccfbc53199f7f88b37c4bea8cbd486345f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 1 Mar 2022 21:50:17 +0800 Subject: [PATCH 02/21] Add initial rate setting --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 1c6fbe88cc..8847420995 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -38,6 +38,16 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; + [SettingSource("Initial rate", "The starting speed of the track")] + public BindableNumber InitialRate { get; } = new BindableDouble + { + MinValue = 0.5, + MaxValue = 2, + Default = 1, + Value = 1, + Precision = 0.01, + }; + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public BindableBool AdjustPitch { get; } = new BindableBool { @@ -54,7 +64,7 @@ namespace osu.Game.Rulesets.Mods private ITrack track; - private readonly List recentRates = Enumerable.Range(0, average_count).Select(x => 1d).ToList(); + private readonly List recentRates = Enumerable.Range(0, average_count).Select(_ => 1d).ToList(); // rates are calculated using the end time of the previous hit object // caching them here for easy access @@ -62,6 +72,7 @@ namespace osu.Game.Rulesets.Mods public ModAdaptiveSpeed() { + InitialRate.BindValueChanged(val => SpeedChange.Value = val.NewValue); AdjustPitch.BindValueChanged(applyPitchAdjustment); } @@ -69,7 +80,10 @@ namespace osu.Game.Rulesets.Mods { this.track = track; + InitialRate.TriggerChange(); AdjustPitch.TriggerChange(); + recentRates.Clear(); + recentRates.AddRange(Enumerable.Range(0, average_count).Select(_ => InitialRate.Value)); } public void ApplyToSample(DrawableSample sample) From c6934b4bce3d6a68ff82da111dacb38731bde479 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Mar 2022 09:53:28 +0800 Subject: [PATCH 03/21] Improve adaptive speed algorithm and add rewind support --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 63 ++++++++++++++++++---- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 8847420995..9a6705ea09 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -1,5 +1,6 @@ // 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; @@ -12,14 +13,15 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap { - private const double fastest_rate = 2f; - - private const double slowest_rate = 0.5f; + // use a wider range so there's still room for adjustment when the initial rate is extreme + private const double fastest_rate = 2.5f; + private const double slowest_rate = 0.4f; /// /// Adjust track rate using the average speed of the last x hits @@ -45,7 +47,7 @@ namespace osu.Game.Rulesets.Mods MaxValue = 2, Default = 1, Value = 1, - Precision = 0.01, + Precision = 0.01 }; [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] @@ -59,17 +61,21 @@ namespace osu.Game.Rulesets.Mods { Default = 1, Value = 1, - Precision = 0.01, + Precision = 0.01 }; private ITrack track; private readonly List recentRates = Enumerable.Range(0, average_count).Select(_ => 1d).ToList(); - // rates are calculated using the end time of the previous hit object + // rate for a hit is calculated using the end time of another hit object earlier in time // caching them here for easy access private readonly Dictionary previousEndTimes = new Dictionary(); + // record the value removed from recentRates when an object is hit + // for rewind support + private readonly Dictionary dequeuedRates = new Dictionary(); + public ModAdaptiveSpeed() { InitialRate.BindValueChanged(val => SpeedChange.Value = val.NewValue); @@ -91,7 +97,7 @@ namespace osu.Game.Rulesets.Mods sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); } - public double ApplyToRate(double time, double rate = 1) => rate; + public double ApplyToRate(double time, double rate = 1) => rate * InitialRate.Value; private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) { @@ -114,8 +120,31 @@ namespace osu.Game.Rulesets.Mods double prevEndTime = previousEndTimes[result.HitObject]; recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, slowest_rate, fastest_rate)); + if (recentRates.Count > average_count) + { + dequeuedRates.Add(result.HitObject, recentRates[0]); recentRates.RemoveAt(0); + } + + SpeedChange.Value = recentRates.Average(); + }; + drawable.OnRevertResult += (o, result) => + { + if (!result.IsHit) return; + if (!previousEndTimes.ContainsKey(result.HitObject)) return; + + if (dequeuedRates.ContainsKey(result.HitObject)) + { + recentRates.Insert(0, dequeuedRates[result.HitObject]); + recentRates.RemoveAt(recentRates.Count - 1); + dequeuedRates.Remove(result.HitObject); + } + else + { + recentRates.Insert(0, InitialRate.Value); + recentRates.RemoveAt(recentRates.Count - 1); + } SpeedChange.Value = recentRates.Average(); }; @@ -123,13 +152,27 @@ namespace osu.Game.Rulesets.Mods public void ApplyToBeatmap(IBeatmap beatmap) { + var endTimes = getEndTimes(beatmap.HitObjects).OrderBy(x => x).ToList(); + for (int i = 1; i < beatmap.HitObjects.Count; i++) { var hitObject = beatmap.HitObjects[i]; - var previousObject = beatmap.HitObjects.Take(i).LastOrDefault(o => !Precision.AlmostBigger(o.GetEndTime(), hitObject.GetEndTime())); + double prevEndTime = endTimes.LastOrDefault(ht => !Precision.AlmostBigger(ht, hitObject.GetEndTime())); - if (previousObject != null) - previousEndTimes.Add(hitObject, previousObject.GetEndTime()); + if (prevEndTime != default) + previousEndTimes.Add(hitObject, prevEndTime); + } + } + + private IEnumerable getEndTimes(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + if (!(hitObject.HitWindows is HitWindows.EmptyHitWindows)) + yield return hitObject.GetEndTime(); + + foreach (double hitTime in getEndTimes(hitObject.NestedHitObjects)) + yield return hitTime; } } } From 6caecf79a012f988a9f08ab94fe6930631e6fd8f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Mar 2022 20:06:28 +0800 Subject: [PATCH 04/21] Use smooth speed change --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 9a6705ea09..b6df56dd76 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -14,10 +15,11 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Mods { - public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap + public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap, IApplicableToHUD { // use a wider range so there's still room for adjustment when the initial rate is extreme private const double fastest_rate = 2.5f; @@ -65,6 +67,7 @@ namespace osu.Game.Rulesets.Mods }; private ITrack track; + private HUDOverlay overlay; private readonly List recentRates = Enumerable.Range(0, average_count).Select(_ => 1d).ToList(); @@ -82,6 +85,12 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.BindValueChanged(applyPitchAdjustment); } + public void ApplyToHUD(HUDOverlay overlay) + { + // this is only used to transform the SpeedChange bindable + this.overlay = overlay; + } + public void ApplyToTrack(ITrack track) { this.track = track; @@ -127,7 +136,7 @@ namespace osu.Game.Rulesets.Mods recentRates.RemoveAt(0); } - SpeedChange.Value = recentRates.Average(); + overlay.TransformBindableTo(SpeedChange, recentRates.Average(), 100); }; drawable.OnRevertResult += (o, result) => { @@ -146,7 +155,7 @@ namespace osu.Game.Rulesets.Mods recentRates.RemoveAt(recentRates.Count - 1); } - SpeedChange.Value = recentRates.Average(); + overlay.TransformBindableTo(SpeedChange, recentRates.Average(), 100); }; } From 17bc7142979d43fedcd23cfe1f67c7d87c6300d0 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Mar 2022 20:48:57 +0800 Subject: [PATCH 05/21] Allow the mod to properly react to nested hit objects --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 41 ++++++++++------------ 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index b6df56dd76..db631520ab 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -123,37 +123,32 @@ namespace osu.Game.Rulesets.Mods { drawable.OnNewResult += (o, result) => { + if (dequeuedRates.ContainsKey(result.HitObject)) return; + if (!result.IsHit) return; + if (!result.Type.AffectsAccuracy()) return; if (!previousEndTimes.ContainsKey(result.HitObject)) return; double prevEndTime = previousEndTimes[result.HitObject]; recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, slowest_rate, fastest_rate)); - if (recentRates.Count > average_count) - { - dequeuedRates.Add(result.HitObject, recentRates[0]); - recentRates.RemoveAt(0); - } + dequeuedRates.Add(result.HitObject, recentRates[0]); + recentRates.RemoveAt(0); overlay.TransformBindableTo(SpeedChange, recentRates.Average(), 100); }; drawable.OnRevertResult += (o, result) => { + if (!dequeuedRates.ContainsKey(result.HitObject)) return; + if (!result.IsHit) return; + if (!result.Type.AffectsAccuracy()) return; if (!previousEndTimes.ContainsKey(result.HitObject)) return; - if (dequeuedRates.ContainsKey(result.HitObject)) - { - recentRates.Insert(0, dequeuedRates[result.HitObject]); - recentRates.RemoveAt(recentRates.Count - 1); - dequeuedRates.Remove(result.HitObject); - } - else - { - recentRates.Insert(0, InitialRate.Value); - recentRates.RemoveAt(recentRates.Count - 1); - } + recentRates.Insert(0, dequeuedRates[result.HitObject]); + recentRates.RemoveAt(recentRates.Count - 1); + dequeuedRates.Remove(result.HitObject); overlay.TransformBindableTo(SpeedChange, recentRates.Average(), 100); }; @@ -161,11 +156,11 @@ namespace osu.Game.Rulesets.Mods public void ApplyToBeatmap(IBeatmap beatmap) { - var endTimes = getEndTimes(beatmap.HitObjects).OrderBy(x => x).ToList(); + var hitObjects = getAllApplicableHitObjects(beatmap.HitObjects).ToList(); + var endTimes = hitObjects.Select(x => x.GetEndTime()).OrderBy(x => x).ToList(); - for (int i = 1; i < beatmap.HitObjects.Count; i++) + foreach (HitObject hitObject in hitObjects) { - var hitObject = beatmap.HitObjects[i]; double prevEndTime = endTimes.LastOrDefault(ht => !Precision.AlmostBigger(ht, hitObject.GetEndTime())); if (prevEndTime != default) @@ -173,15 +168,15 @@ namespace osu.Game.Rulesets.Mods } } - private IEnumerable getEndTimes(IEnumerable hitObjects) + private IEnumerable getAllApplicableHitObjects(IEnumerable hitObjects) { foreach (var hitObject in hitObjects) { if (!(hitObject.HitWindows is HitWindows.EmptyHitWindows)) - yield return hitObject.GetEndTime(); + yield return hitObject; - foreach (double hitTime in getEndTimes(hitObject.NestedHitObjects)) - yield return hitTime; + foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects)) + yield return nested; } } } From d335a2229f448a7a9f30f6b144910aafe398be83 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Mar 2022 21:07:57 +0800 Subject: [PATCH 06/21] Tweak `average_count` --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index db631520ab..7c92bd8141 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mods /// /// Adjust track rate using the average speed of the last x hits /// - private const int average_count = 10; + private const int average_count = 6; public override string Name => "Adaptive Speed"; From 55737226a31ba2f190abeb88953a99e6d704be54 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 10:18:36 +0800 Subject: [PATCH 07/21] Use `Enumerable.Repeat` --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 7c92bd8141..f155005f67 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mods private ITrack track; private HUDOverlay overlay; - private readonly List recentRates = Enumerable.Range(0, average_count).Select(_ => 1d).ToList(); + private readonly List recentRates = Enumerable.Repeat(1d, average_count).ToList(); // rate for a hit is calculated using the end time of another hit object earlier in time // caching them here for easy access @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mods InitialRate.TriggerChange(); AdjustPitch.TriggerChange(); recentRates.Clear(); - recentRates.AddRange(Enumerable.Range(0, average_count).Select(_ => InitialRate.Value)); + recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, average_count)); } public void ApplyToSample(DrawableSample sample) From ff7f65de2712200496499ac647fc7fa8a9193cda Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 10:43:04 +0800 Subject: [PATCH 08/21] Extract duplicated conditionals --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index f155005f67..69d5a956d9 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; @@ -124,10 +125,7 @@ namespace osu.Game.Rulesets.Mods drawable.OnNewResult += (o, result) => { if (dequeuedRates.ContainsKey(result.HitObject)) return; - - if (!result.IsHit) return; - if (!result.Type.AffectsAccuracy()) return; - if (!previousEndTimes.ContainsKey(result.HitObject)) return; + if (!shouldProcessResult(result)) return; double prevEndTime = previousEndTimes[result.HitObject]; @@ -141,10 +139,7 @@ namespace osu.Game.Rulesets.Mods drawable.OnRevertResult += (o, result) => { if (!dequeuedRates.ContainsKey(result.HitObject)) return; - - if (!result.IsHit) return; - if (!result.Type.AffectsAccuracy()) return; - if (!previousEndTimes.ContainsKey(result.HitObject)) return; + if (!shouldProcessResult(result)) return; recentRates.Insert(0, dequeuedRates[result.HitObject]); recentRates.RemoveAt(recentRates.Count - 1); @@ -179,5 +174,14 @@ namespace osu.Game.Rulesets.Mods yield return nested; } } + + private bool shouldProcessResult(JudgementResult result) + { + if (!result.IsHit) return false; + if (!result.Type.AffectsAccuracy()) return false; + if (!previousEndTimes.ContainsKey(result.HitObject)) return false; + + return true; + } } } From 95a40c5dc561fbc55d7d4fe60ce6a34c57326149 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 10:43:30 +0800 Subject: [PATCH 09/21] Remove pointless comment --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 69d5a956d9..946d25172c 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -111,7 +111,6 @@ namespace osu.Game.Rulesets.Mods private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) { - // remove existing old adjustment track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); From 51258dbab4fca3c77cdeb767bf616fab633b16e2 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 11:21:20 +0800 Subject: [PATCH 10/21] Use binary search in `ApplyToBeatmap` --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 946d25172c..276f3466f4 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -9,7 +9,6 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Judgements; @@ -151,14 +150,16 @@ namespace osu.Game.Rulesets.Mods public void ApplyToBeatmap(IBeatmap beatmap) { var hitObjects = getAllApplicableHitObjects(beatmap.HitObjects).ToList(); - var endTimes = hitObjects.Select(x => x.GetEndTime()).OrderBy(x => x).ToList(); + var endTimes = hitObjects.Select(x => x.GetEndTime()).OrderBy(x => x).Distinct().ToList(); foreach (HitObject hitObject in hitObjects) { - double prevEndTime = endTimes.LastOrDefault(ht => !Precision.AlmostBigger(ht, hitObject.GetEndTime())); + int index = endTimes.BinarySearch(hitObject.GetEndTime()); + if (index < 0) index = ~index; // BinarySearch returns the next larger element in bitwise complement if there's no exact match + index -= 1; - if (prevEndTime != default) - previousEndTimes.Add(hitObject, prevEndTime); + if (index >= 0) + previousEndTimes.Add(hitObject, endTimes[index]); } } From 09254407fe894b0da3dae1a4ac9cacfa0bac8be2 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 12:02:39 +0800 Subject: [PATCH 11/21] Interpolate speed change using `IUpdatableByPlayfield` --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 33 +++++++++++----------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 276f3466f4..0ad31ebb9c 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -1,25 +1,24 @@ // 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.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { - public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap, IApplicableToHUD + public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap, IUpdatableByPlayfield { // use a wider range so there's still room for adjustment when the initial rate is extreme private const double fastest_rate = 2.5f; @@ -62,12 +61,11 @@ namespace osu.Game.Rulesets.Mods public BindableNumber SpeedChange { get; } = new BindableDouble { Default = 1, - Value = 1, - Precision = 0.01 + Value = 1 }; private ITrack track; - private HUDOverlay overlay; + private double targetRate = 1d; private readonly List recentRates = Enumerable.Repeat(1d, average_count).ToList(); @@ -81,16 +79,14 @@ namespace osu.Game.Rulesets.Mods public ModAdaptiveSpeed() { - InitialRate.BindValueChanged(val => SpeedChange.Value = val.NewValue); + InitialRate.BindValueChanged(val => + { + SpeedChange.Value = val.NewValue; + targetRate = val.NewValue; + }); AdjustPitch.BindValueChanged(applyPitchAdjustment); } - public void ApplyToHUD(HUDOverlay overlay) - { - // this is only used to transform the SpeedChange bindable - this.overlay = overlay; - } - public void ApplyToTrack(ITrack track) { this.track = track; @@ -106,6 +102,11 @@ namespace osu.Game.Rulesets.Mods sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); } + public void Update(Playfield playfield) + { + SpeedChange.Value = Interpolation.DampContinuously(SpeedChange.Value, targetRate, 50, playfield.Clock.ElapsedFrameTime); + } + public double ApplyToRate(double time, double rate = 1) => rate * InitialRate.Value; private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) @@ -132,7 +133,7 @@ namespace osu.Game.Rulesets.Mods dequeuedRates.Add(result.HitObject, recentRates[0]); recentRates.RemoveAt(0); - overlay.TransformBindableTo(SpeedChange, recentRates.Average(), 100); + targetRate = recentRates.Average(); }; drawable.OnRevertResult += (o, result) => { @@ -143,7 +144,7 @@ namespace osu.Game.Rulesets.Mods recentRates.RemoveAt(recentRates.Count - 1); dequeuedRates.Remove(result.HitObject); - overlay.TransformBindableTo(SpeedChange, recentRates.Average(), 100); + targetRate = recentRates.Average(); }; } From ae71dcceeb0d7478210eaf631f62732352e4debd Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 13:03:53 +0800 Subject: [PATCH 12/21] Convert comments to xmldoc --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 0ad31ebb9c..5a2edfa17d 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -69,12 +69,15 @@ namespace osu.Game.Rulesets.Mods private readonly List recentRates = Enumerable.Repeat(1d, average_count).ToList(); - // rate for a hit is calculated using the end time of another hit object earlier in time - // caching them here for easy access + /// + /// Rate for a hit is calculated using the end time of another hit object earlier in time, + /// caching them here for easy access + /// private readonly Dictionary previousEndTimes = new Dictionary(); - // record the value removed from recentRates when an object is hit - // for rewind support + /// + /// Record the value removed from when an object is hit for rewind support + /// private readonly Dictionary dequeuedRates = new Dictionary(); public ModAdaptiveSpeed() From 9c2aa511943c3b89279beb00aa954e1204539d98 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 13:07:30 +0800 Subject: [PATCH 13/21] Rename `applyPitchAdjustment` to `adjustPitchChanged` --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 5a2edfa17d..2e77b7c6fc 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mods SpeedChange.Value = val.NewValue; targetRate = val.NewValue; }); - AdjustPitch.BindValueChanged(applyPitchAdjustment); + AdjustPitch.BindValueChanged(adjustPitchChanged); } public void ApplyToTrack(ITrack track) @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mods public double ApplyToRate(double time, double rate = 1) => rate * InitialRate.Value; - private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) + private void adjustPitchChanged(ValueChangedEvent adjustPitchSetting) { track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); From 4ce2044e4c842f50f0beb7ea62a8b2b493439a37 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 3 Mar 2022 13:09:29 +0800 Subject: [PATCH 14/21] Reorder members --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 2e77b7c6fc..b3a318e307 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -1,5 +1,6 @@ // 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; @@ -112,16 +113,6 @@ namespace osu.Game.Rulesets.Mods public double ApplyToRate(double time, double rate = 1) => rate * InitialRate.Value; - private void adjustPitchChanged(ValueChangedEvent adjustPitchSetting) - { - track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - - track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); - } - - private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) - => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; - public void ApplyToDrawableHitObject(DrawableHitObject drawable) { drawable.OnNewResult += (o, result) => @@ -167,6 +158,16 @@ namespace osu.Game.Rulesets.Mods } } + private void adjustPitchChanged(ValueChangedEvent adjustPitchSetting) + { + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); + + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + } + + private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) + => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; + private IEnumerable getAllApplicableHitObjects(IEnumerable hitObjects) { foreach (var hitObject in hitObjects) From ffaf5b729f4da8af9b5061f2ea037c1e4ad5d640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Mar 2022 17:07:36 +0100 Subject: [PATCH 15/21] Move and reword docs of allowable rate range constants --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index b3a318e307..bde85c60ad 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -21,10 +21,6 @@ namespace osu.Game.Rulesets.Mods { public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap, IUpdatableByPlayfield { - // use a wider range so there's still room for adjustment when the initial rate is extreme - private const double fastest_rate = 2.5f; - private const double slowest_rate = 0.4f; - /// /// Adjust track rate using the average speed of the last x hits /// @@ -61,10 +57,18 @@ namespace osu.Game.Rulesets.Mods public BindableNumber SpeedChange { get; } = new BindableDouble { + MinValue = min_allowable_rate, + MaxValue = max_allowable_rate, Default = 1, Value = 1 }; + // The two constants below denote the maximum allowable range of rates that `SpeedChange` can take. + // The range is purposefully wider than the range of values that `InitialRate` allows + // in order to give some leeway for change even when extreme initial rates are chosen. + private const double min_allowable_rate = 0.4f; + private const double max_allowable_rate = 2.5f; + private ITrack track; private double targetRate = 1d; @@ -122,7 +126,7 @@ namespace osu.Game.Rulesets.Mods double prevEndTime = previousEndTimes[result.HitObject]; - recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, slowest_rate, fastest_rate)); + recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, min_allowable_rate, max_allowable_rate)); dequeuedRates.Add(result.HitObject, recentRates[0]); recentRates.RemoveAt(0); From 3797871aa08aa836134a450fc03ab2431b3a47c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Mar 2022 17:25:49 +0100 Subject: [PATCH 16/21] Add extended documentation of adaptive speed mod machinations --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 73 ++++++++++++++++------ 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index bde85c60ad..78839ea692 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -21,11 +21,6 @@ namespace osu.Game.Rulesets.Mods { public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap, IUpdatableByPlayfield { - /// - /// Adjust track rate using the average speed of the last x hits - /// - private const int average_count = 6; - public override string Name => "Adaptive Speed"; public override string Acronym => "AS"; @@ -55,6 +50,10 @@ namespace osu.Game.Rulesets.Mods Value = true }; + /// + /// The instantaneous rate of the track. + /// Every frame this mod will attempt to smoothly adjust this to meet . + /// public BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = min_allowable_rate, @@ -72,18 +71,52 @@ namespace osu.Game.Rulesets.Mods private ITrack track; private double targetRate = 1d; - private readonly List recentRates = Enumerable.Repeat(1d, average_count).ToList(); + /// + /// The number of most recent track rates (approximated from how early/late each object was hit relative to the previous object) + /// which should be averaged to calculate the instantaneous value of . + /// + private const int recent_rate_count = 6; /// - /// Rate for a hit is calculated using the end time of another hit object earlier in time, - /// caching them here for easy access + /// Stores the most recent approximated track rates + /// which are averaged to calculate the instantaneous value of . /// - private readonly Dictionary previousEndTimes = new Dictionary(); + /// + /// This list is used as a double-ended queue with fixed capacity + /// (items can be enqueued/dequeued at either end of the list). + /// When time is elapsing forward, items are dequeued from the start and enqueued onto the end of the list. + /// When time is being rewound, items are dequeued from the end and enqueued onto the start of the list. + /// + private readonly List recentRates = Enumerable.Repeat(1d, recent_rate_count).ToList(); /// - /// Record the value removed from when an object is hit for rewind support + /// For each given in the map, this dictionary maps the object onto the latest end time of any other object + /// that precedes the end time of the given object. + /// This can be loosely interpreted as the end time of the preceding hit object in rulesets that do not have overlapping hit objects. /// - private readonly Dictionary dequeuedRates = new Dictionary(); + private readonly Dictionary precedingEndTimes = new Dictionary(); + + /// + /// For each given in the map, this dictionary maps the object onto the approximated track rate with which the user hit it. + /// + /// + /// + /// The approximation is calculated as follows: + /// + /// + /// Consider a hitobject which ends at 1000ms, and assume that its preceding hitobject ends at 500ms. + /// This gives a time difference of 1000 - 500 = 500ms. + /// + /// + /// Now assume that the user hit this object at 980ms rather than 1000ms. + /// When compared to the preceding hitobject, this gives 980 - 500 = 480ms. + /// + /// + /// With the above assumptions, the player is rushing / hitting early, which means that the track should speed up to match. + /// Therefore, the approximated target rate for this object would be equal to 500 / 480 * . + /// + /// + private readonly Dictionary approximatedRates = new Dictionary(); public ModAdaptiveSpeed() { @@ -102,7 +135,7 @@ namespace osu.Game.Rulesets.Mods InitialRate.TriggerChange(); AdjustPitch.TriggerChange(); recentRates.Clear(); - recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, average_count)); + recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count)); } public void ApplyToSample(DrawableSample sample) @@ -121,26 +154,26 @@ namespace osu.Game.Rulesets.Mods { drawable.OnNewResult += (o, result) => { - if (dequeuedRates.ContainsKey(result.HitObject)) return; + if (approximatedRates.ContainsKey(result.HitObject)) return; if (!shouldProcessResult(result)) return; - double prevEndTime = previousEndTimes[result.HitObject]; + double prevEndTime = precedingEndTimes[result.HitObject]; recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, min_allowable_rate, max_allowable_rate)); - dequeuedRates.Add(result.HitObject, recentRates[0]); + approximatedRates.Add(result.HitObject, recentRates[0]); recentRates.RemoveAt(0); targetRate = recentRates.Average(); }; drawable.OnRevertResult += (o, result) => { - if (!dequeuedRates.ContainsKey(result.HitObject)) return; + if (!approximatedRates.ContainsKey(result.HitObject)) return; if (!shouldProcessResult(result)) return; - recentRates.Insert(0, dequeuedRates[result.HitObject]); + recentRates.Insert(0, approximatedRates[result.HitObject]); recentRates.RemoveAt(recentRates.Count - 1); - dequeuedRates.Remove(result.HitObject); + approximatedRates.Remove(result.HitObject); targetRate = recentRates.Average(); }; @@ -158,7 +191,7 @@ namespace osu.Game.Rulesets.Mods index -= 1; if (index >= 0) - previousEndTimes.Add(hitObject, endTimes[index]); + precedingEndTimes.Add(hitObject, endTimes[index]); } } @@ -188,7 +221,7 @@ namespace osu.Game.Rulesets.Mods { if (!result.IsHit) return false; if (!result.Type.AffectsAccuracy()) return false; - if (!previousEndTimes.ContainsKey(result.HitObject)) return false; + if (!precedingEndTimes.ContainsKey(result.HitObject)) return false; return true; } From fcefd3c72544ce153c870734de1b384b411ee1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Mar 2022 17:39:47 +0100 Subject: [PATCH 17/21] Fix slightly wrong references in xmldocs --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 78839ea692..c28283d0bb 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -73,13 +73,13 @@ namespace osu.Game.Rulesets.Mods /// /// The number of most recent track rates (approximated from how early/late each object was hit relative to the previous object) - /// which should be averaged to calculate the instantaneous value of . + /// which should be averaged to calculate . /// private const int recent_rate_count = 6; /// /// Stores the most recent approximated track rates - /// which are averaged to calculate the instantaneous value of . + /// which are averaged to calculate the value of . /// /// /// This list is used as a double-ended queue with fixed capacity From b66af7edf419823ae63105bb48d78640d615c001 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 4 Mar 2022 11:03:57 +0800 Subject: [PATCH 18/21] Rename `approximatedRates` to `ratesForRewinding` and update xmldoc --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 45 ++++++++++++---------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index c28283d0bb..428d605a99 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -87,21 +87,9 @@ namespace osu.Game.Rulesets.Mods /// When time is elapsing forward, items are dequeued from the start and enqueued onto the end of the list. /// When time is being rewound, items are dequeued from the end and enqueued onto the start of the list. /// - private readonly List recentRates = Enumerable.Repeat(1d, recent_rate_count).ToList(); - - /// - /// For each given in the map, this dictionary maps the object onto the latest end time of any other object - /// that precedes the end time of the given object. - /// This can be loosely interpreted as the end time of the preceding hit object in rulesets that do not have overlapping hit objects. - /// - private readonly Dictionary precedingEndTimes = new Dictionary(); - - /// - /// For each given in the map, this dictionary maps the object onto the approximated track rate with which the user hit it. - /// /// /// - /// The approximation is calculated as follows: + /// The track rate approximation is calculated as follows: /// /// /// Consider a hitobject which ends at 1000ms, and assume that its preceding hitobject ends at 500ms. @@ -116,7 +104,21 @@ namespace osu.Game.Rulesets.Mods /// Therefore, the approximated target rate for this object would be equal to 500 / 480 * . /// /// - private readonly Dictionary approximatedRates = new Dictionary(); + private readonly List recentRates = Enumerable.Repeat(1d, recent_rate_count).ToList(); + + /// + /// For each given in the map, this dictionary maps the object onto the latest end time of any other object + /// that precedes the end time of the given object. + /// This can be loosely interpreted as the end time of the preceding hit object in rulesets that do not have overlapping hit objects. + /// + private readonly Dictionary precedingEndTimes = new Dictionary(); + + /// + /// For each given in the map, this dictionary maps the object onto the track rate dequeued from + /// (i.e. the oldest value in the queue) when the object is hit. If the hit is then reverted, + /// the mapped value can be re-introduced to to properly rewind the queue. + /// + private readonly Dictionary ratesForRewinding = new Dictionary(); public ModAdaptiveSpeed() { @@ -154,26 +156,27 @@ namespace osu.Game.Rulesets.Mods { drawable.OnNewResult += (o, result) => { - if (approximatedRates.ContainsKey(result.HitObject)) return; + if (ratesForRewinding.ContainsKey(result.HitObject)) return; if (!shouldProcessResult(result)) return; double prevEndTime = precedingEndTimes[result.HitObject]; - recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, min_allowable_rate, max_allowable_rate)); - - approximatedRates.Add(result.HitObject, recentRates[0]); + ratesForRewinding.Add(result.HitObject, recentRates[0]); recentRates.RemoveAt(0); + recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, min_allowable_rate, max_allowable_rate)); + targetRate = recentRates.Average(); }; drawable.OnRevertResult += (o, result) => { - if (!approximatedRates.ContainsKey(result.HitObject)) return; + if (!ratesForRewinding.ContainsKey(result.HitObject)) return; if (!shouldProcessResult(result)) return; - recentRates.Insert(0, approximatedRates[result.HitObject]); + recentRates.Insert(0, ratesForRewinding[result.HitObject]); + ratesForRewinding.Remove(result.HitObject); + recentRates.RemoveAt(recentRates.Count - 1); - approximatedRates.Remove(result.HitObject); targetRate = recentRates.Average(); }; From f72c9a1f410de9b456567b2366b4dd4dce7eb624 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 4 Mar 2022 11:48:48 +0800 Subject: [PATCH 19/21] Cap speed change per hit and apply a speed decrease on miss --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 428d605a99..e7ca1d3150 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -65,8 +65,16 @@ namespace osu.Game.Rulesets.Mods // The two constants below denote the maximum allowable range of rates that `SpeedChange` can take. // The range is purposefully wider than the range of values that `InitialRate` allows // in order to give some leeway for change even when extreme initial rates are chosen. - private const double min_allowable_rate = 0.4f; - private const double max_allowable_rate = 2.5f; + private const double min_allowable_rate = 0.4d; + private const double max_allowable_rate = 2.5d; + + // The two constants below denote the maximum allowable change in rate caused by a single hit + // This prevents sudden jolts caused by a badly-timed hit. + private const double min_allowable_rate_change = 0.8d; + private const double max_allowable_rate_change = 1.25d; + + // Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast. + private const double rate_change_on_miss = 0.95d; private ITrack track; private double targetRate = 1d; @@ -159,12 +167,10 @@ namespace osu.Game.Rulesets.Mods if (ratesForRewinding.ContainsKey(result.HitObject)) return; if (!shouldProcessResult(result)) return; - double prevEndTime = precedingEndTimes[result.HitObject]; - ratesForRewinding.Add(result.HitObject, recentRates[0]); recentRates.RemoveAt(0); - recentRates.Add(Math.Clamp((result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime) * SpeedChange.Value, min_allowable_rate, max_allowable_rate)); + recentRates.Add(Math.Clamp(getRelativeRateChange(result) * SpeedChange.Value, min_allowable_rate, max_allowable_rate)); targetRate = recentRates.Average(); }; @@ -222,11 +228,23 @@ namespace osu.Game.Rulesets.Mods private bool shouldProcessResult(JudgementResult result) { - if (!result.IsHit) return false; if (!result.Type.AffectsAccuracy()) return false; if (!precedingEndTimes.ContainsKey(result.HitObject)) return false; return true; } + + private double getRelativeRateChange(JudgementResult result) + { + if (!result.IsHit) + return rate_change_on_miss; + + double prevEndTime = precedingEndTimes[result.HitObject]; + return Math.Clamp( + (result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime), + min_allowable_rate_change, + max_allowable_rate_change + ); + } } } From 8b8b54b58ffc2f82c22e775cffc855964c1d77e3 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 5 Mar 2022 21:48:57 +0800 Subject: [PATCH 20/21] Scale rate adjustments based on hit timing consistency and tweak some related numbers --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 29 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index e7ca1d3150..1115b95e6f 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -70,8 +70,8 @@ namespace osu.Game.Rulesets.Mods // The two constants below denote the maximum allowable change in rate caused by a single hit // This prevents sudden jolts caused by a badly-timed hit. - private const double min_allowable_rate_change = 0.8d; - private const double max_allowable_rate_change = 1.25d; + private const double min_allowable_rate_change = 0.9d; + private const double max_allowable_rate_change = 1.11d; // Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast. private const double rate_change_on_miss = 0.95d; @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Mods /// The number of most recent track rates (approximated from how early/late each object was hit relative to the previous object) /// which should be averaged to calculate . /// - private const int recent_rate_count = 6; + private const int recent_rate_count = 8; /// /// Stores the most recent approximated track rates @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Mods recentRates.Add(Math.Clamp(getRelativeRateChange(result) * SpeedChange.Value, min_allowable_rate, max_allowable_rate)); - targetRate = recentRates.Average(); + updateTargetRate(); }; drawable.OnRevertResult += (o, result) => { @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Mods recentRates.RemoveAt(recentRates.Count - 1); - targetRate = recentRates.Average(); + updateTargetRate(); }; } @@ -246,5 +246,24 @@ namespace osu.Game.Rulesets.Mods max_allowable_rate_change ); } + + /// + /// Update based on the values in . + /// + private void updateTargetRate() + { + // Compare values in recentRates to see how consistent the player's speed is + // If the player hits half of the notes too fast and the other half too slow: Abs(consistency) = 0 + // If the player hits all their notes too fast or too slow: Abs(consistency) = recent_rate_count - 1 + int consistency = 0; + + for (int i = 1; i < recentRates.Count; i++) + { + consistency += Math.Sign(recentRates[i] - recentRates[i - 1]); + } + + // Scale the rate adjustment based on consistency + targetRate = Interpolation.Lerp(targetRate, recentRates.Average(), Math.Abs(consistency) / (recent_rate_count - 1d)); + } } } From 520d2d6cfa4b7e3206083e5c68ea8c04879d10b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Mar 2022 16:06:39 +0900 Subject: [PATCH 21/21] Fix beatmap carousel panels accepting input while marked as not-visible This is an issue as carousel panels manage their own animated state. If they are marked as not-visible (done at a higher level, from filtering or update pathways) but clicked while fading out, they will animate back to a visible state but not be marked as visible. No tests for this one as it's probably not worthwhile to test (and hard to do so). Manual testing can be done with the following patch: ```diff diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c3d340ac61..3372242acc 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -255,7 +255,7 @@ private void beatmapSetsChanged(IRealmCollection sender, ChangeS } foreach (int i in changes.NewModifiedIndices) - UpdateBeatmapSet(sender[i].Detach()); + Scheduler.AddDelayed(() => UpdateBeatmapSet(sender[i].Detach()), 100, true); foreach (int i in changes.InsertedIndices) UpdateBeatmapSet(sender[i].Detach()); ``` - Enter gameplay and adjust beatmap offset then return to song select and click the flashing panel. OR - Enter editor and save then return to song select and click the flashing panel. Closes https://github.com/ppy/osu/discussions/17171. --- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index cde3edad39..75bcdedec4 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -17,6 +17,9 @@ namespace osu.Game.Screens.Select.Carousel public override bool IsPresent => base.IsPresent || Item?.Visible == true; + public override bool HandlePositionalInput => Item?.Visible == true; + public override bool PropagatePositionalInputSubTree => Item?.Visible == true; + public readonly CarouselHeader Header; ///