From c9b205afeb64f721e228ee51adb8eabc0cb93327 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 1 Mar 2022 21:12:06 +0800 Subject: [PATCH 01/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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/48] 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 78a3b5961e8590e85a9303fb362d3ab8d1993c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 15:48:33 +0100 Subject: [PATCH 21/48] Implement basic difficulty multiplier display --- .../TestSceneDifficultyMultiplierDisplay.cs | 40 +++++ .../Mods/DifficultyMultiplierDisplay.cs | 166 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneDifficultyMultiplierDisplay.cs create mode 100644 osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDifficultyMultiplierDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDifficultyMultiplierDisplay.cs new file mode 100644 index 0000000000..cd84f8b380 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDifficultyMultiplierDisplay.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneDifficultyMultiplierDisplay : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [Test] + public void TestDifficultyMultiplierDisplay() + { + DifficultyMultiplierDisplay multiplierDisplay = null; + + AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + AddStep("set multiplier below 1", () => multiplierDisplay.Current.Value = 0.5); + AddStep("set multiplier to 1", () => multiplierDisplay.Current.Value = 1); + AddStep("set multiplier above 1", () => multiplierDisplay.Current.Value = 1.5); + + AddSliderStep("set multiplier", 0, 2, 1d, multiplier => + { + if (multiplierDisplay != null) + multiplierDisplay.Current.Value = multiplier; + }); + } + } +} diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs new file mode 100644 index 0000000000..1d4aa9e0f5 --- /dev/null +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -0,0 +1,166 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public class DifficultyMultiplierDisplay : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(1); + + private readonly Box underlayBackground; + private readonly Box contentBackground; + private readonly FillFlowContainer multiplierFlow; + private readonly OsuSpriteText multiplierText; + + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + private const float height = 42; + private const float transition_duration = 200; + + public DifficultyMultiplierDisplay() + { + Height = height; + AutoSizeAxes = Axes.X; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Masking = true, + CornerRadius = ModPanel.CORNER_RADIUS, + Shear = new Vector2(ModPanel.SHEAR_X, 0), + Children = new Drawable[] + { + underlayBackground = new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = height + ModPanel.CORNER_RADIUS + }, + new GridContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, height) + }, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Masking = true, + CornerRadius = ModPanel.CORNER_RADIUS, + Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Horizontal = 18 }, + Shear = new Vector2(-ModPanel.SHEAR_X, 0), + Text = "Difficulty Multiplier", + Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) + } + } + }, + multiplierFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shear = new Vector2(-ModPanel.SHEAR_X, 0), + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2, 0), + Children = new Drawable[] + { + multiplierText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) + }, + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.Times, + Size = new Vector2(7), + Margin = new MarginPadding { Top = 1 } + } + } + } + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load() + { + contentBackground.Colour = colourProvider.Background4; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + current.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + private void updateState() + { + multiplierText.Text = current.Value.ToLocalisableString(@"N1"); + + if (Current.IsDefault) + { + underlayBackground.FadeColour(colourProvider.Background3, transition_duration, Easing.OutQuint); + multiplierFlow.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + } + else + { + var backgroundColour = Current.Value < 1 + ? colours.ForModType(ModType.DifficultyReduction) + : colours.ForModType(ModType.DifficultyIncrease); + + underlayBackground.FadeColour(backgroundColour, transition_duration, Easing.OutQuint); + multiplierFlow.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint); + } + } + } +} From c25d7a1c75f0b964ff0434f58f621a2a22645d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 16:02:46 +0100 Subject: [PATCH 22/48] Use rolling counter for multiplier display --- .../Mods/DifficultyMultiplierDisplay.cs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index 1d4aa9e0f5..3fbba0b8ed 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -9,8 +9,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osuTK; @@ -24,12 +26,15 @@ namespace osu.Game.Overlays.Mods set => current.Current = value; } - private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(1); + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(1) + { + Precision = 0.1 + }; private readonly Box underlayBackground; private readonly Box contentBackground; private readonly FillFlowContainer multiplierFlow; - private readonly OsuSpriteText multiplierText; + private readonly MultiplierCounter multiplierCounter; [Resolved] private OsuColour colours { get; set; } @@ -107,11 +112,11 @@ namespace osu.Game.Overlays.Mods Spacing = new Vector2(2, 0), Children = new Drawable[] { - multiplierText = new OsuSpriteText + multiplierCounter = new MultiplierCounter { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) + Current = { BindTarget = Current } }, new SpriteIcon { @@ -141,12 +146,11 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); current.BindValueChanged(_ => updateState(), true); FinishTransforms(true); + multiplierCounter.StopRolling(); } private void updateState() { - multiplierText.Text = current.Value.ToLocalisableString(@"N1"); - if (Current.IsDefault) { underlayBackground.FadeColour(colourProvider.Background3, transition_duration, Easing.OutQuint); @@ -162,5 +166,17 @@ namespace osu.Game.Overlays.Mods multiplierFlow.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint); } } + + private class MultiplierCounter : RollingCounter + { + protected override double RollingDuration => 500; + + protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N1"); + + protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) + }; + } } } From 019f4d965de8165675aae9027105b2e54730f7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Mar 2022 22:55:55 +0100 Subject: [PATCH 23/48] Show two decimal digits on mod multiplier rather than one --- osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index 3fbba0b8ed..e10edb1ebc 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Mods private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(1) { - Precision = 0.1 + Precision = 0.01 }; private readonly Box underlayBackground; @@ -43,6 +43,7 @@ namespace osu.Game.Overlays.Mods private OverlayColourProvider colourProvider { get; set; } private const float height = 42; + private const float multiplier_value_area_width = 56; private const float transition_duration = 200; public DifficultyMultiplierDisplay() @@ -64,7 +65,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, - Width = height + ModPanel.CORNER_RADIUS + Width = multiplier_value_area_width + ModPanel.CORNER_RADIUS }, new GridContainer { @@ -73,7 +74,7 @@ namespace osu.Game.Overlays.Mods ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, height) + new Dimension(GridSizeMode.Absolute, multiplier_value_area_width) }, Content = new[] { @@ -171,7 +172,7 @@ namespace osu.Game.Overlays.Mods { protected override double RollingDuration => 500; - protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N1"); + protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N2"); protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText { From 643f68e8447d7cf946508c7632fab0e239303eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Mar 2022 23:11:20 +0100 Subject: [PATCH 24/48] Better annotate initial rolling counter value set --- osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index e10edb1ebc..4fc3a904fa 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -147,7 +147,9 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); current.BindValueChanged(_ => updateState(), true); FinishTransforms(true); - multiplierCounter.StopRolling(); + // required to prevent the counter initially rolling up from 0 to 1 + // due to `Current.Value` having a nonstandard default value of 1. + multiplierCounter.SetCountWithoutRolling(Current.Value); } private void updateState() From ded84cab3ff0f5a8dd0e79639fa77b828a70cc61 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 8 Mar 2022 11:45:16 +0800 Subject: [PATCH 25/48] Separate randomisation and object positioning logic --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 101 +++++++++++++-------- 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 9b9ebcad04..1d0442438e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "It never gets boring!"; private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; + private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2; /// /// Number of previous hitobjects to be shifted together when another object is being moved. @@ -44,20 +45,15 @@ namespace osu.Game.Rulesets.Osu.Mods rng = new Random((int)Seed.Value); - RandomObjectInfo previous = null; + var randomObjects = randomiseObjects(hitObjects); - float rateOfChangeMultiplier = 0; + RandomObjectInfo previous = null; for (int i = 0; i < hitObjects.Count; i++) { var hitObject = hitObjects[i]; - var current = new RandomObjectInfo(hitObject); - - // rateOfChangeMultiplier only changes every 5 iterations in a combo - // to prevent shaky-line-shaped streams - if (hitObject.IndexInCurrentCombo % 5 == 0) - rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; + var current = randomObjects[i]; if (hitObject is Spinner) { @@ -65,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Mods continue; } - applyRandomisation(rateOfChangeMultiplier, previous, current); + applyRandomisation(getAbsoluteAngle(hitObjects, i - 1), previous, current); // Move hit objects back into the playfield if they are outside of it Vector2 shift = Vector2.Zero; @@ -101,45 +97,72 @@ namespace osu.Game.Rulesets.Osu.Mods } } + private List randomiseObjects(IEnumerable hitObjects) + { + var randomObjects = new List(); + RandomObjectInfo previous = null; + float rateOfChangeMultiplier = 0; + + foreach (OsuHitObject hitObject in hitObjects) + { + var current = new RandomObjectInfo(hitObject); + randomObjects.Add(current); + + // rateOfChangeMultiplier only changes every 5 iterations in a combo + // to prevent shaky-line-shaped streams + if (hitObject.IndexInCurrentCombo % 5 == 0) + rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; + + if (previous == null) + { + current.Distance = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); + current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); + } + else + { + current.Distance = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); + + // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) + // is proportional to the distance between the last and the current hit object + // to allow jumps and prevent too sharp turns during streams. + + // Allow maximum jump angle when jump distance is more than half of playfield diagonal length + current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.Distance / (playfield_diagonal * 0.5f)); + } + + previous = current; + } + + return randomObjects; + } + + private float getAbsoluteAngle(IReadOnlyList hitObjects, int hitObjectIndex) + { + if (hitObjectIndex < 0) return 0; + + Vector2 previousPosition = hitObjectIndex == 0 ? playfield_centre : hitObjects[hitObjectIndex - 1].EndPosition; + Vector2 relativePosition = hitObjects[hitObjectIndex].Position - previousPosition; + return (float)Math.Atan2(relativePosition.Y, relativePosition.X); + } + /// /// Returns the final position of the hit object /// /// Final position of the hit object - private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current) + private void applyRandomisation(float previousAbsoluteAngle, RandomObjectInfo previous, RandomObjectInfo current) { - if (previous == null) - { - var playfieldSize = OsuPlayfield.BASE_SIZE; - - current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); - current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y); - - return; - } - - float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); - - // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) - // is proportional to the distance between the last and the current hit object - // to allow jumps and prevent too sharp turns during streams. - - // Allow maximum jump angle when jump distance is more than half of playfield diagonal length - double randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, distanceToPrev / (playfield_diagonal * 0.5f)); - - current.AngleRad = (float)randomAngleRad + previous.AngleRad; - if (current.AngleRad < 0) - current.AngleRad += 2 * (float)Math.PI; + float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; var posRelativeToPrev = new Vector2( - distanceToPrev * (float)Math.Cos(current.AngleRad), - distanceToPrev * (float)Math.Sin(current.AngleRad) + current.Distance * (float)Math.Cos(absoluteAngle), + current.Distance * (float)Math.Sin(absoluteAngle) ); - posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev); + Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre; - current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); - current.PositionRandomised = previous.EndPositionRandomised + posRelativeToPrev; + current.PositionRandomised = lastEndPosition + posRelativeToPrev; } /// @@ -287,7 +310,8 @@ namespace osu.Game.Rulesets.Osu.Mods private class RandomObjectInfo { - public float AngleRad { get; set; } + public float RelativeAngle { get; set; } + public float Distance { get; set; } public Vector2 PositionOriginal { get; } public Vector2 PositionRandomised { get; set; } @@ -299,7 +323,6 @@ namespace osu.Game.Rulesets.Osu.Mods { PositionRandomised = PositionOriginal = hitObject.Position; EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition; - AngleRad = 0; } } } From 8cfeffc085def6f9f063b3c6d7bf75e78e864bc7 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 8 Mar 2022 11:50:30 +0800 Subject: [PATCH 26/48] Extract a major part of `ApplyToBeatmap` to a new method --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 103 +++++++++++---------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 1d0442438e..76183fe0aa 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -47,54 +47,7 @@ namespace osu.Game.Rulesets.Osu.Mods var randomObjects = randomiseObjects(hitObjects); - RandomObjectInfo previous = null; - - for (int i = 0; i < hitObjects.Count; i++) - { - var hitObject = hitObjects[i]; - - var current = randomObjects[i]; - - if (hitObject is Spinner) - { - previous = null; - continue; - } - - applyRandomisation(getAbsoluteAngle(hitObjects, i - 1), previous, current); - - // Move hit objects back into the playfield if they are outside of it - Vector2 shift = Vector2.Zero; - - switch (hitObject) - { - case HitCircle circle: - shift = clampHitCircleToPlayfield(circle, current); - break; - - case Slider slider: - shift = clampSliderToPlayfield(slider, current); - break; - } - - if (shift != Vector2.Zero) - { - var toBeShifted = new List(); - - for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--) - { - // only shift hit circles - if (!(hitObjects[j] is HitCircle)) break; - - toBeShifted.Add(hitObjects[j]); - } - - if (toBeShifted.Count > 0) - applyDecreasingShift(toBeShifted, shift); - } - - previous = current; - } + applyRandomisation(hitObjects, randomObjects); } private List randomiseObjects(IEnumerable hitObjects) @@ -136,6 +89,58 @@ namespace osu.Game.Rulesets.Osu.Mods return randomObjects; } + private void applyRandomisation(IReadOnlyList hitObjects, IReadOnlyList randomObjects) + { + RandomObjectInfo previous = null; + + for (int i = 0; i < hitObjects.Count; i++) + { + var hitObject = hitObjects[i]; + + var current = randomObjects[i]; + + if (hitObject is Spinner) + { + previous = null; + continue; + } + + computeRandomisedPosition(getAbsoluteAngle(hitObjects, i - 1), previous, current); + + // Move hit objects back into the playfield if they are outside of it + Vector2 shift = Vector2.Zero; + + switch (hitObject) + { + case HitCircle circle: + shift = clampHitCircleToPlayfield(circle, current); + break; + + case Slider slider: + shift = clampSliderToPlayfield(slider, current); + break; + } + + if (shift != Vector2.Zero) + { + var toBeShifted = new List(); + + for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--) + { + // only shift hit circles + if (!(hitObjects[j] is HitCircle)) break; + + toBeShifted.Add(hitObjects[j]); + } + + if (toBeShifted.Count > 0) + applyDecreasingShift(toBeShifted, shift); + } + + previous = current; + } + } + private float getAbsoluteAngle(IReadOnlyList hitObjects, int hitObjectIndex) { if (hitObjectIndex < 0) return 0; @@ -149,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// Returns the final position of the hit object /// /// Final position of the hit object - private void applyRandomisation(float previousAbsoluteAngle, RandomObjectInfo previous, RandomObjectInfo current) + private void computeRandomisedPosition(float previousAbsoluteAngle, RandomObjectInfo previous, RandomObjectInfo current) { float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; From ae1c65c38dfd48e57be107f31da80ac7be62db5f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 8 Mar 2022 12:07:10 +0800 Subject: [PATCH 27/48] Add xmldoc --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 39 ++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 76183fe0aa..babc4311ee 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -50,6 +50,11 @@ namespace osu.Game.Rulesets.Osu.Mods applyRandomisation(hitObjects, randomObjects); } + /// + /// Randomise the position of each hit object and return a list of s describing how each hit object should be placed. + /// + /// A list of s to have their positions randomised. + /// A list of s describing how each hit object should be placed. private List randomiseObjects(IEnumerable hitObjects) { var randomObjects = new List(); @@ -89,6 +94,11 @@ namespace osu.Game.Rulesets.Osu.Mods return randomObjects; } + /// + /// Reposition the hit objects according to the information in . + /// + /// The hit objects to be repositioned. + /// A list of describing how each hit object should be placed. private void applyRandomisation(IReadOnlyList hitObjects, IReadOnlyList randomObjects) { RandomObjectInfo previous = null; @@ -141,6 +151,12 @@ namespace osu.Game.Rulesets.Osu.Mods } } + /// + /// Get the absolute angle of a vector pointing from the previous hit object to the one denoted by . + /// + /// A list of all hit objects in the beatmap. + /// The hit object that the vector should point to. + /// The absolute angle of the aforementioned vector. private float getAbsoluteAngle(IReadOnlyList hitObjects, int hitObjectIndex) { if (hitObjectIndex < 0) return 0; @@ -151,9 +167,11 @@ namespace osu.Game.Rulesets.Osu.Mods } /// - /// Returns the final position of the hit object + /// Compute the randomised position of a hit object while attempting to keep it inside the playfield. /// - /// Final position of the hit object + /// The direction of movement of the player's cursor before it starts to approach the current hit object. + /// The representing the hit object immediately preceding the current one. + /// The representing the hit object to have the randomised position computed for. private void computeRandomisedPosition(float previousAbsoluteAngle, RandomObjectInfo previous, RandomObjectInfo current) { float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; @@ -315,7 +333,24 @@ namespace osu.Game.Rulesets.Osu.Mods private class RandomObjectInfo { + /// + /// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle. + /// + /// + /// of the first hit object in a beatmap represents the absolute angle from playfield center to the object. + /// + /// + /// If is 0, the player's cursor doesn't need to change its direction of movement when passing + /// the previous object to reach this one. + /// public float RelativeAngle { get; set; } + + /// + /// The jump distance from the previous hit object to this one. + /// + /// + /// of the first hit object in a beatmap is relative to the playfield center. + /// public float Distance { get; set; } public Vector2 PositionOriginal { get; } From 0718a55ad0d86e9c5715f67499b6f8b8e2b9f5d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Mar 2022 16:06:42 +0900 Subject: [PATCH 28/48] Add flow to allow recovery after running an older release (with a different realm database version) As brought up in https://github.com/ppy/osu/discussions/17148 --- osu.Game/Database/RealmAccess.cs | 86 +++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index af7c485c57..e7ca045702 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -105,6 +105,8 @@ namespace osu.Game.Database public Realm Realm => ensureUpdateRealm(); + private const string realm_extension = @".realm"; + private Realm ensureUpdateRealm() { if (isSendingNotificationResetEvents) @@ -149,11 +151,18 @@ namespace osu.Game.Database Filename = filename; - const string realm_extension = @".realm"; - if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) Filename += realm_extension; + string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; + + // Attempt to recover a newer database version if available. + if (storage.Exists(newerVersionFilename)) + { + Logger.Log(@"A newer realm database has been found, attempting recovery...", LoggingTarget.Database); + attemptRecoverFromFile(newerVersionFilename); + } + try { // This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date. @@ -161,15 +170,78 @@ namespace osu.Game.Database } catch (Exception e) { - Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made."); + // See https://github.com/realm/realm-core/blob/master/src%2Frealm%2Fobject-store%2Fobject_store.cpp#L1016-L1022 + // This is the best way we can detect a schema version downgrade. + if (e.Message.StartsWith(@"Provided schema version", StringComparison.Ordinal)) + { + Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data."); - CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}"); - storage.Delete(Filename); + // If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about. + if (!storage.Exists(newerVersionFilename)) + CreateBackup(newerVersionFilename); + + storage.Delete(Filename); + } + else + { + Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made."); + CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}"); + storage.Delete(Filename); + } cleanupPendingDeletions(); } } + private void attemptRecoverFromFile(string recoveryFilename) + { + Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database); + + // First check the user hasn't started to use the database that is in place.. + try + { + using (var realm = Realm.GetInstance(getConfiguration())) + { + if (realm.All().Any()) + { + Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database); + Logger.Log(@"To perform recovery, delete client.realm while osu! is not running.", LoggingTarget.Database); + return; + } + } + } + catch + { + // Even if reading the in place database fails, still attempt to recover. + } + + // Then check that the database we are about to attempt recovery can actually be recovered on this version.. + try + { + using (var realm = Realm.GetInstance(getConfiguration(recoveryFilename))) + { + // Don't need to do anything, just check that opening the realm works correctly. + } + } + catch + { + Logger.Log(@"Recovery aborted as the newer version could not be loaded by this osu! version.", LoggingTarget.Database); + return; + } + + // For extra safety, also store the temporarily-used database which we are about to replace. + CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_newer_version_before_recovery{realm_extension}"); + + storage.Delete(Filename); + + using (var inputStream = storage.GetStream(recoveryFilename)) + using (var outputStream = storage.GetStream(Filename, FileAccess.Write, FileMode.Create)) + inputStream.CopyTo(outputStream); + + storage.Delete(recoveryFilename); + Logger.Log(@"Recovery complete!", LoggingTarget.Database); + } + private void cleanupPendingDeletions() { using (var realm = getRealmInstance()) @@ -476,7 +548,7 @@ namespace osu.Game.Database } } - private RealmConfiguration getConfiguration() + private RealmConfiguration getConfiguration(string? filename = null) { // This is currently the only usage of temporary files at the osu! side. // If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase). @@ -484,7 +556,7 @@ namespace osu.Game.Database if (!Directory.Exists(tempPathLocation)) Directory.CreateDirectory(tempPathLocation); - return new RealmConfiguration(storage.GetFullPath(Filename, true)) + return new RealmConfiguration(storage.GetFullPath(filename ?? Filename, true)) { SchemaVersion = schema_version, MigrationCallback = onMigration, From 6565c95b176a8808cd3d8fd1987e31bdbae3952a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Mar 2022 18:19:54 +0900 Subject: [PATCH 29/48] Remove unused variable --- osu.Game/Database/RealmAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index e7ca045702..f0d4011ab8 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -218,7 +218,7 @@ namespace osu.Game.Database // Then check that the database we are about to attempt recovery can actually be recovered on this version.. try { - using (var realm = Realm.GetInstance(getConfiguration(recoveryFilename))) + using (Realm.GetInstance(getConfiguration(recoveryFilename))) { // Don't need to do anything, just check that opening the realm works correctly. } From f5cd9676353c9bdcd6c6a30d0069612e36528cc2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 19:07:39 +0900 Subject: [PATCH 30/48] Fix scores not being recalculated in beatmap listing --- osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs | 3 ++- osu.Game/Scoring/ScoreManager.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 00dedc892b..6f07b20049 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -79,7 +79,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var beatmapInfo = new BeatmapInfo { MaxCombo = apiBeatmap.MaxCombo, - Status = apiBeatmap.Status + Status = apiBeatmap.Status, + MD5Hash = apiBeatmap.MD5Hash }; scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 963c4a77ca..87345a06fb 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -132,7 +132,7 @@ namespace osu.Game.Scoring public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { // TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place. - if (string.IsNullOrEmpty(score.BeatmapInfo.Hash)) + if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash)) return score.TotalScore; int beatmapMaxCombo; From d13a66a96cb147a7dba1fdbfa1be75f8533440e7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 20:11:56 +0900 Subject: [PATCH 31/48] Rework test scene by only relying on OnlineID --- .../TestScenePlaylistsResultsScreen.cs | 76 +++++-------------- 1 file changed, 21 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 161624413d..67894bab38 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -33,16 +33,25 @@ namespace osu.Game.Tests.Visual.Playlists private TestResultsScreen resultsScreen; - private int currentScoreId; + private int lowestScoreId; // Score ID of the lowest score in the list. + private int highestScoreId; // Score ID of the highest score in the list. + private bool requestComplete; private int totalCount; + private ScoreInfo userScore; [SetUp] public void Setup() => Schedule(() => { - currentScoreId = 1; + lowestScoreId = 1; + highestScoreId = 1; requestComplete = false; totalCount = 0; + + userScore = TestResources.CreateTestScoreInfo(); + userScore.TotalScore = 0; + userScore.Statistics = new Dictionary(); + bindHandler(); // beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring. @@ -53,15 +62,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowWithUserScore() { - ScoreInfo userScore = null; - - AddStep("bind user score info handler", () => - { - userScore = TestResources.CreateTestScoreInfo(); - userScore.OnlineID = currentScoreId++; - - bindHandler(userScore: userScore); - }); + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); @@ -81,15 +82,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowUserScoreWithDelay() { - ScoreInfo userScore = null; - - AddStep("bind user score info handler", () => - { - userScore = TestResources.CreateTestScoreInfo(); - userScore.OnlineID = currentScoreId++; - - bindHandler(true, userScore); - }); + AddStep("bind user score info handler", () => bindHandler(true, userScore)); createResults(() => userScore); @@ -124,7 +117,7 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); waitForDisplay(); - AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); } } @@ -132,15 +125,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestFetchWhenScrolledToTheLeft() { - ScoreInfo userScore = null; - - AddStep("bind user score info handler", () => - { - userScore = TestResources.CreateTestScoreInfo(); - userScore.OnlineID = currentScoreId++; - - bindHandler(userScore: userScore); - }); + AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); createResults(() => userScore); @@ -156,7 +141,7 @@ namespace osu.Game.Tests.Visual.Playlists AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); waitForDisplay(); - AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() >= beforePanelCount + scores_per_result); AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); } } @@ -245,16 +230,13 @@ namespace osu.Game.Tests.Visual.Playlists { var multiplayerUserScore = new MultiplayerScore { - ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++), + ID = highestScoreId, Accuracy = userScore.Accuracy, - EndedAt = userScore.Date, Passed = userScore.Passed, Rank = userScore.Rank, Position = real_user_position, MaxCombo = userScore.MaxCombo, - TotalScore = userScore.TotalScore, User = userScore.User, - Statistics = userScore.Statistics, ScoresAround = new MultiplayerScoresAround { Higher = new MultiplayerScores(), @@ -268,38 +250,32 @@ namespace osu.Game.Tests.Visual.Playlists { multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore { - ID = currentScoreId++, + ID = --highestScoreId, Accuracy = userScore.Accuracy, - EndedAt = userScore.Date, Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, - TotalScore = userScore.TotalScore - i, User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, - Statistics = userScore.Statistics }); multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore { - ID = currentScoreId++, + ID = ++lowestScoreId, Accuracy = userScore.Accuracy, - EndedAt = userScore.Date, Passed = true, Rank = userScore.Rank, MaxCombo = userScore.MaxCombo, - TotalScore = userScore.TotalScore + i, User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, - Statistics = userScore.Statistics }); totalCount += 2; @@ -315,33 +291,23 @@ namespace osu.Game.Tests.Visual.Playlists { var result = new IndexedMultiplayerScores(); - long startTotalScore = req.Cursor?.Properties["total_score"].ToObject() ?? 1000000; string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; for (int i = 1; i <= scores_per_result; i++) { result.Scores.Add(new MultiplayerScore { - ID = currentScoreId++, + ID = sort == "score_asc" ? --highestScoreId : ++lowestScoreId, Accuracy = 1, - EndedAt = DateTimeOffset.Now, Passed = true, Rank = ScoreRank.X, MaxCombo = 1000, - TotalScore = startTotalScore + (sort == "score_asc" ? i : -i), User = new APIUser { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, - Statistics = new Dictionary - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 } - } }); totalCount++; @@ -367,7 +333,7 @@ namespace osu.Game.Tests.Visual.Playlists { Properties = new Dictionary { - { "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") } + { "sort", JToken.FromObject(scores.Scores[^1].ID > scores.Scores[^2].ID ? "score_asc" : "score_desc") } } }; } From e9a2d235420c87ef3c45d06e82997fc7248a426f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 23:35:35 +0900 Subject: [PATCH 32/48] Fix score order related test failure --- .../Visual/Ranking/TestSceneScorePanelList.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index e786b85f78..c65587d433 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Models; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Tests.Resources; @@ -208,13 +210,19 @@ namespace osu.Game.Tests.Visual.Ranking public void TestKeyboardNavigation() { var lowestScore = TestResources.CreateTestScoreInfo(); - lowestScore.MaxCombo = 100; + lowestScore.OnlineID = 3; + lowestScore.TotalScore = 0; + lowestScore.Statistics = new Dictionary(); var middleScore = TestResources.CreateTestScoreInfo(); - middleScore.MaxCombo = 200; + middleScore.OnlineID = 2; + middleScore.TotalScore = 0; + middleScore.Statistics = new Dictionary(); var highestScore = TestResources.CreateTestScoreInfo(); - highestScore.MaxCombo = 300; + highestScore.OnlineID = 1; + highestScore.TotalScore = 0; + highestScore.Statistics = new Dictionary(); createListStep(() => new ScorePanelList()); From 286bafe326cf1f9b5bf35cd1e9c1f1867ad09781 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Mar 2022 01:18:53 +0900 Subject: [PATCH 33/48] Refactor multiple `TestScenePlaySongSelect` test methods to be resilient to slow realm callbacks --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index e2b50e38c2..c0c1e6b7a4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -284,14 +284,13 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestDummy() { createSongSelect(); - AddAssert("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap); + AddUntilStep("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap); AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap); addManyTestMaps(); - AddWaitStep("wait for select", 3); - AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); + AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); } [Test] @@ -299,9 +298,8 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); addManyTestMaps(); - AddWaitStep("wait for add", 3); - AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); + AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title)); @@ -571,6 +569,8 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); + AddUntilStep("wait for selection", () => !Beatmap.IsDefault); + AddStep("press ctrl+enter", () => { InputManager.PressKey(Key.ControlLeft); @@ -605,6 +605,8 @@ namespace osu.Game.Tests.Visual.SongSelect addRulesetImportStep(0); createSongSelect(); + AddUntilStep("wait for selection", () => !Beatmap.IsDefault); + DrawableCarouselBeatmapSet set = null; AddStep("Find the DrawableCarouselBeatmapSet", () => { @@ -844,6 +846,8 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); + AddUntilStep("wait for selection", () => !Beatmap.IsDefault); + AddStep("present score", () => { // this beatmap change should be overridden by the present. From 233c8232d3c782f22f5352a442ed6c76de25a73c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Mar 2022 01:21:46 +0900 Subject: [PATCH 34/48] Fix `TestSceneTopLocalRank.TestHighScoreSet` not waiting for potentially slow realm callback As brought to light by https://gist.github.com/smoogipoo/56eda7ab56b9d1966556f2ca7a80a847. --- osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 39680d157b..7bef7c8fce 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelect }); AddUntilStep("Became present", () => topLocalRank.IsPresent); - AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.B); + AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.B); AddStep("Add higher score for current user", () => { @@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect scoreManager.Import(testScoreInfo2); }); - AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.S); + AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.S); } } } From 3ced5e7904877e7b989984bff414b1ce2c83dd47 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Mar 2022 13:09:33 +0800 Subject: [PATCH 35/48] Rename `Distance` to `DistanceFromPrevious` --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index babc4311ee..e450127488 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -73,19 +73,19 @@ namespace osu.Game.Rulesets.Osu.Mods if (previous == null) { - current.Distance = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); + current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); } else { - current.Distance = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); + current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); // The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object) // is proportional to the distance between the last and the current hit object // to allow jumps and prevent too sharp turns during streams. // Allow maximum jump angle when jump distance is more than half of playfield diagonal length - current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.Distance / (playfield_diagonal * 0.5f)); + current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f)); } previous = current; @@ -177,8 +177,8 @@ namespace osu.Game.Rulesets.Osu.Mods float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; var posRelativeToPrev = new Vector2( - current.Distance * (float)Math.Cos(absoluteAngle), - current.Distance * (float)Math.Sin(absoluteAngle) + current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), + current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) ); Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre; @@ -349,9 +349,9 @@ namespace osu.Game.Rulesets.Osu.Mods /// The jump distance from the previous hit object to this one. /// /// - /// of the first hit object in a beatmap is relative to the playfield center. + /// of the first hit object in a beatmap is relative to the playfield center. /// - public float Distance { get; set; } + public float DistanceFromPrevious { get; set; } public Vector2 PositionOriginal { get; } public Vector2 PositionRandomised { get; set; } From e3cf2c6acd3e0aecd307e458acbe5f251258f488 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 9 Mar 2022 13:18:57 +0800 Subject: [PATCH 36/48] Merge `getAbsoluteAngle` into `computeRandomisedPosition` --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 36 ++++++++++------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index e450127488..895dc9a506 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Graphics.Primitives; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -115,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Mods continue; } - computeRandomisedPosition(getAbsoluteAngle(hitObjects, i - 1), previous, current); + computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null); // Move hit objects back into the playfield if they are outside of it Vector2 shift = Vector2.Zero; @@ -151,29 +152,23 @@ namespace osu.Game.Rulesets.Osu.Mods } } - /// - /// Get the absolute angle of a vector pointing from the previous hit object to the one denoted by . - /// - /// A list of all hit objects in the beatmap. - /// The hit object that the vector should point to. - /// The absolute angle of the aforementioned vector. - private float getAbsoluteAngle(IReadOnlyList hitObjects, int hitObjectIndex) - { - if (hitObjectIndex < 0) return 0; - - Vector2 previousPosition = hitObjectIndex == 0 ? playfield_centre : hitObjects[hitObjectIndex - 1].EndPosition; - Vector2 relativePosition = hitObjects[hitObjectIndex].Position - previousPosition; - return (float)Math.Atan2(relativePosition.Y, relativePosition.X); - } - /// /// Compute the randomised position of a hit object while attempting to keep it inside the playfield. /// - /// The direction of movement of the player's cursor before it starts to approach the current hit object. - /// The representing the hit object immediately preceding the current one. /// The representing the hit object to have the randomised position computed for. - private void computeRandomisedPosition(float previousAbsoluteAngle, RandomObjectInfo previous, RandomObjectInfo current) + /// The representing the hit object immediately preceding the current one. + /// The representing the hit object immediately preceding the one. + private void computeRandomisedPosition(RandomObjectInfo current, [CanBeNull] RandomObjectInfo previous, [CanBeNull] RandomObjectInfo beforePrevious) { + float previousAbsoluteAngle = 0f; + + if (previous != null) + { + Vector2 earliestPosition = beforePrevious == null ? playfield_centre : beforePrevious.HitObject.EndPosition; + Vector2 relativePosition = previous.HitObject.Position - earliestPosition; + previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + } + float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle; var posRelativeToPrev = new Vector2( @@ -359,10 +354,13 @@ namespace osu.Game.Rulesets.Osu.Mods public Vector2 EndPositionOriginal { get; } public Vector2 EndPositionRandomised { get; set; } + public OsuHitObject HitObject { get; } + public RandomObjectInfo(OsuHitObject hitObject) { PositionRandomised = PositionOriginal = hitObject.Position; EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition; + HitObject = hitObject; } } } From ad0ca5673a445f5a1e7fb38da44af8050ae7fa38 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Mar 2022 14:39:02 +0900 Subject: [PATCH 37/48] Fix avatar not clickable after watching replay --- osu.Game/Users/Drawables/ClickableAvatar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index e3cfaf1d14..34c87568a1 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -64,7 +64,7 @@ namespace osu.Game.Users.Drawables private void openProfile() { - if (user?.Id > 1) + if (user?.Id > 1 || !string.IsNullOrEmpty(user?.Username)) game?.ShowUser(user); } From b07a1e8d09b50bd8511dabcbc75c49a55439687c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Mar 2022 15:38:00 +0900 Subject: [PATCH 38/48] Fix unable to copy playlist rooms without first opening --- osu.Game/Online/Rooms/Room.cs | 19 +-------- .../OnlinePlay/Lounge/DrawableLoungeRoom.cs | 2 +- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 39 +++++++++++++++++++ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index a33150fe08..543b176b51 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -10,12 +10,11 @@ using osu.Game.IO.Serialization.Converters; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms.RoomStatuses; -using osu.Game.Utils; namespace osu.Game.Online.Rooms { [JsonObject(MemberSerialization.OptIn)] - public class Room : IDeepCloneable + public class Room { [Cached] [JsonProperty("id")] @@ -153,22 +152,6 @@ namespace osu.Game.Online.Rooms Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue)); } - /// - /// Create a copy of this room without online information. - /// Should be used to create a local copy of a room for submitting in the future. - /// - public Room DeepClone() - { - var copy = new Room(); - - copy.CopyFrom(this); - - // ID must be unset as we use this as a marker for whether this is a client-side (not-yet-created) room or not. - copy.RoomID.Value = null; - - return copy; - } - public void CopyFrom(Room other) { RoomID.Value = other.RoomID.Value; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 27743e709f..7baa346c6f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - lounge?.Open(Room.DeepClone()); + lounge?.OpenCopy(Room); }) }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index cd1c8a0a64..08b4ae9db2 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -20,6 +20,7 @@ using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -63,6 +64,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private IBindable ruleset { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + [CanBeNull] private IDisposable joiningRoomOperation { get; set; } @@ -310,6 +314,41 @@ namespace osu.Game.Screens.OnlinePlay.Lounge }); }); + /// + /// Copies a room and opens it as a fresh (not-yet-created) one. + /// + /// The room to copy. + public void OpenCopy(Room room) + { + Debug.Assert(room.RoomID.Value != null); + + if (joiningRoomOperation != null) + return; + + joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); + + var req = new GetRoomRequest(room.RoomID.Value.Value); + + req.Success += r => + { + // ID must be unset as we use this as a marker for whether this is a client-side (not-yet-created) room or not. + r.RoomID.Value = null; + + Open(r); + + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; + }; + + req.Failure += _ => + { + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; + }; + + api.Queue(req); + } + /// /// Push a room as a new subscreen. /// From 520d2d6cfa4b7e3206083e5c68ea8c04879d10b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Mar 2022 16:06:39 +0900 Subject: [PATCH 39/48] 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; /// From 4839bd804466fb410884ee1f94eec40a84c86785 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Mar 2022 16:47:47 +0900 Subject: [PATCH 40/48] Notify if copying room fails Co-authored-by: Dean Herbert --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 08b4ae9db2..fb8647284f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -340,8 +340,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge joiningRoomOperation = null; }; - req.Failure += _ => + req.Failure += exception => { + Logger.Error(exception, "Couldn't create a copy of this room."); joiningRoomOperation?.Dispose(); joiningRoomOperation = null; }; From 75c6a676b42b4d51bd15b74a2254e0a559269169 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Mar 2022 16:58:36 +0900 Subject: [PATCH 41/48] Apply `nullable` to `OsuModRandom` rather than using jetbrains annotations --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 895dc9a506..8ccfbf0da5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -1,10 +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 enable + using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Graphics.Primitives; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -33,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// private const int preceding_hitobjects_to_shift = 10; - private Random rng; + private Random? rng; public void ApplyToBeatmap(IBeatmap beatmap) { @@ -58,8 +60,10 @@ namespace osu.Game.Rulesets.Osu.Mods /// A list of s describing how each hit object should be placed. private List randomiseObjects(IEnumerable hitObjects) { + Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects"); + var randomObjects = new List(); - RandomObjectInfo previous = null; + RandomObjectInfo? previous = null; float rateOfChangeMultiplier = 0; foreach (OsuHitObject hitObject in hitObjects) @@ -102,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// A list of describing how each hit object should be placed. private void applyRandomisation(IReadOnlyList hitObjects, IReadOnlyList randomObjects) { - RandomObjectInfo previous = null; + RandomObjectInfo? previous = null; for (int i = 0; i < hitObjects.Count; i++) { @@ -158,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// The representing the hit object to have the randomised position computed for. /// The representing the hit object immediately preceding the current one. /// The representing the hit object immediately preceding the one. - private void computeRandomisedPosition(RandomObjectInfo current, [CanBeNull] RandomObjectInfo previous, [CanBeNull] RandomObjectInfo beforePrevious) + private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious) { float previousAbsoluteAngle = 0f; From 353b251d3887e6a71dd82ada40a8205fdf6611a4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 9 Mar 2022 17:46:41 +0900 Subject: [PATCH 42/48] Attempt to merge conditional expression Hoping to fix CI error, caused by older R# version. --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 8ccfbf0da5..7479c3120a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (previous != null) { - Vector2 earliestPosition = beforePrevious == null ? playfield_centre : beforePrevious.HitObject.EndPosition; + Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; Vector2 relativePosition = previous.HitObject.Position - earliestPosition; previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); } From 5fb51b578f52adf7a426f5871de0c1f0a833cc3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Mar 2022 19:09:50 +0900 Subject: [PATCH 43/48] Update dependencies Mainly for a `Clowd.Squirrel` bump to fix https://github.com/ppy/osu/discussions/17190. --- osu.Desktop/osu.Desktop.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game/osu.Game.csproj | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 32ead231c7..a06484214b 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index bd4c3d3345..4ce29ab5c7 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index acf1e8470b..0bcf533653 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -8,7 +8,7 @@ - + WinExe diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d86fbc693e..c106c373c4 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -19,13 +19,13 @@ - + - - - + + + @@ -35,10 +35,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + From 8539f619c50eb06dc320fc1902e2c3cd0afd23f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Mar 2022 21:16:40 +0900 Subject: [PATCH 44/48] Update framework --- osu.Android.props | 2 +- osu.iOS.props | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 5b26b8f36e..c2788f9a48 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c37692f0d8..e485c69096 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From b59117caf643d04e1f35d8814811dc7c3bce2791 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Mar 2022 21:42:02 +0900 Subject: [PATCH 45/48] Update silly iOS dependencies --- osu.iOS.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.iOS.props b/osu.iOS.props index e485c69096..86cf1b229c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -79,7 +79,7 @@ - + @@ -89,6 +89,6 @@ - + From d2983b74d53714176600b02387a8f2370b2d264f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 9 Mar 2022 20:15:34 +0100 Subject: [PATCH 46/48] Bump Moq in mobile test projects for parity --- osu.Game.Tests.Android/osu.Game.Tests.Android.csproj | 2 +- osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index b45a3249ff..afafec6b1f 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -79,7 +79,7 @@ - + 5.0.0 diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index 97df9b2cd5..05b3cad6da 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -48,7 +48,7 @@ - + From 5cb058a17d636ef70e2ea833a9401fd6dd41d332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 9 Mar 2022 20:17:46 +0100 Subject: [PATCH 47/48] Bump Realm in Android test project for parity --- osu.Android.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Android.props b/osu.Android.props index c2788f9a48..839f7882bf 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -56,6 +56,6 @@ - + From c36badab4bf9c636c9c5e516c805923d96ba5b31 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 10 Mar 2022 10:26:09 +0900 Subject: [PATCH 48/48] Add per-ruleset score multipliers for classic scoring --- osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs | 1 + osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs | 2 ++ osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 2 ++ osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 2 ++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 ++++++- 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 2cc05826b4..9cd03dc869 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -7,5 +7,6 @@ namespace osu.Game.Rulesets.Catch.Scoring { public class CatchScoreProcessor : ScoreProcessor { + protected override double ClassicScoreMultiplier => 28; } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 48b377c794..e0b19d87e8 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -10,5 +10,7 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double DefaultAccuracyPortion => 0.99; protected override double DefaultComboPortion => 0.01; + + protected override double ClassicScoreMultiplier => 16; } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 44118227d9..df38f0204a 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { + protected override double ClassicScoreMultiplier => 36; + protected override HitEvent CreateHitEvent(JudgementResult result) => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 1829ea2513..849b9c14bd 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -10,5 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring protected override double DefaultAccuracyPortion => 0.75; protected override double DefaultComboPortion => 0.25; + + protected override double ClassicScoreMultiplier => 22; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index d5a5aa4592..7ca8a0fecf 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -76,6 +76,11 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual double DefaultComboPortion => 0.7; + /// + /// An arbitrary multiplier to scale scores in the scoring mode. + /// + protected virtual double ClassicScoreMultiplier => 36; + private readonly double accuracyPortion; private readonly double comboPortion; @@ -246,7 +251,7 @@ namespace osu.Game.Rulesets.Scoring // This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring. // The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes. double scaledStandardised = GetScore(ScoringMode.Standardised, accuracyRatio, comboRatio, statistics) / max_score; - return Math.Pow(scaledStandardised * totalHitObjects, 2) * 36; + return Math.Pow(scaledStandardised * totalHitObjects, 2) * ClassicScoreMultiplier; } }