From c6934b4bce3d6a68ff82da111dacb38731bde479 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 2 Mar 2022 09:53:28 +0800 Subject: [PATCH] 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; } } }