diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index b3d530e3affa..c5d59a47a1c7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -38,5 +38,7 @@ protected override double StrainValueAt(DifficultyHitObject current) return currentStrain; } + + public override bool IsRelevant => withSliders; } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDefaultSongProgressGraph.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDefaultSongProgressGraph.cs index 66671a506f39..4134d8ffae8e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDefaultSongProgressGraph.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDefaultSongProgressGraph.cs @@ -56,7 +56,7 @@ private void displayRandomValues() for (double i = 0; i < 5000; i += RNG.NextDouble() * 10 + i / 1000) objects.Add(new HitObject { StartTime = i }); - graph.Objects = objects; + graph.SetFromObjects(objects); } private partial class TestSongProgressGraph : DefaultSongProgressGraph diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index 99f0ffb9d078..b0284f347eb2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -91,8 +91,8 @@ public void TestBasic() AddToggleStep("toggle graph", b => { - applyToDefaultProgress(s => s.ShowGraph.Value = b); - applyToArgonProgress(s => s.ShowGraph.Value = b); + applyToDefaultProgress(s => s.GraphType.Value = b ? DifficultyGraphType.ObjectDensity : DifficultyGraphType.None); + applyToArgonProgress(s => s.GraphType.Value = b ? DifficultyGraphType.ObjectDensity : DifficultyGraphType.None); }); AddStep("set white background", () => background.FadeColour(Color4.White, 200, Easing.OutQuint)); diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 871faf5906f1..274dd5bd6c9d 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -156,6 +156,14 @@ public Task> GetTimedDifficultyAttributesAsync(I updateScheduler); } + public Task> GetSectionDifficultiesAsync(IWorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken cancellationToken = default) + { + return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateSectionDifficulties(mods, cancellationToken).ToList(), + cancellationToken, + TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, + updateScheduler); + } + /// /// Updates all tracked using the current ruleset and mods. /// diff --git a/osu.Game/Localisation/HUD/SongProgressStrings.cs b/osu.Game/Localisation/HUD/SongProgressStrings.cs index 332f15cb174e..6909cb028694 100644 --- a/osu.Game/Localisation/HUD/SongProgressStrings.cs +++ b/osu.Game/Localisation/HUD/SongProgressStrings.cs @@ -10,14 +10,14 @@ public static class SongProgressStrings private const string prefix = @"osu.Game.Resources.Localisation.HUD.SongProgress"; /// - /// "Show difficulty graph" + /// "Difficulty graph type" /// - public static LocalisableString ShowGraph => new TranslatableString(getKey(@"show_graph"), "Show difficulty graph"); + public static LocalisableString GraphType => new TranslatableString(getKey(@"graph_type"), "Difficulty graph type"); /// - /// "Whether a graph displaying difficulty throughout the beatmap should be shown" + /// "Type of a graph displaying difficulty throughout the beatmap" /// - public static LocalisableString ShowGraphDescription => new TranslatableString(getKey(@"show_graph_description"), "Whether a graph displaying difficulty throughout the beatmap should be shown"); + public static LocalisableString GraphTypeDescription => new TranslatableString(getKey(@"graph_type_description"), "Type of a graph displaying difficulty throughout the beatmap"); /// /// "Show time" diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 63b27243d02a..6bc47a95c48d 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -133,6 +133,55 @@ public List CalculateTimed([NotNull] IEnumerable return attribs; } + /// + /// Calculates the difficulty of section of the beatmap with no mods applied. + /// + /// The cancellation token. + /// Per-skill array where each value represents difficulty of this section in certain skill. + public IEnumerable CalculateSectionDifficulties(CancellationToken cancellationToken = default) + => CalculateSectionDifficulties(Array.Empty(), cancellationToken); + + /// + /// Calculates the difficulty of section of the beatmap using a specific mod combination. + /// + /// The mods that should be applied to the beatmap. + /// The cancellation token. + /// Per-skill array where each value represents difficulty of this section in certain skill. + public IEnumerable CalculateSectionDifficulties([NotNull] IEnumerable mods, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + preProcess(mods, cancellationToken); + + var skills = CreateSkills(Beatmap, playableMods, clockRate); + StrainSkill[] relevantSkills = skills.OfType().Where(s => s.IsRelevant).ToArray(); + var hitObjects = getDifficultyHitObjects(); + + if (!hitObjects.Any()) + return Enumerable.Empty(); + + // Add sections before first object to preserve correct bounds + foreach (var skill in relevantSkills) + skill.AddEmptySections(hitObjects.First().StartTime, Beatmap.HitObjects.First().StartTime / clockRate); + + foreach (var hitObject in hitObjects) + { + foreach (var skill in relevantSkills) + { + cancellationToken.ThrowIfCancellationRequested(); + skill.Process(hitObject); + } + } + + // Add sections up to the end time of last object to preserve correct bounds + foreach (var skill in relevantSkills) + skill.AddEmptySections(Beatmap.HitObjects.Last().GetEndTime() / clockRate); + + var strainsForSkills = relevantSkills + .Select(skill => skill.GetCurrentStrainPeaks().ToArray()); + + return strainsForSkills; + } + /// /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. /// diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 8b8892113b8a..458772e03162 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -37,5 +37,10 @@ protected Skill(Mod[] mods) /// Returns the calculated difficulty value representing all s that have been processed up to this point. /// public abstract double DifficultyValue(); + + /// + /// Returns true if this Skill is relevant to star rating calculation. + /// + public virtual bool IsRelevant => true; } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index b07e8399c024..037bcfcd22ce 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -60,6 +60,22 @@ public sealed override void Process(DifficultyHitObject current) currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak); } + /// + /// Adds empty strain sections + /// + public void AddEmptySections(double time, double? currentSectionOverride = null) + { + if (currentSectionOverride.HasValue) + currentSectionEnd = Math.Ceiling(currentSectionOverride.Value / SectionLength) * SectionLength; + + while (time > currentSectionEnd) + { + saveCurrentPeak(); + currentSectionPeak = 0; // This is wrong, but there's no way get decay from this class + currentSectionEnd += SectionLength; + } + } + /// /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty. /// diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 8dc5d603529a..ab3e551eeb70 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -24,8 +24,8 @@ public partial class ArgonSongProgress : SongProgress private const float bar_height = 10; - [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))] - public Bindable ShowGraph { get; } = new BindableBool(true); + [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.GraphType), nameof(SongProgressStrings.GraphTypeDescription))] + public Bindable GraphType { get; } = new Bindable(DifficultyGraphType.ObjectDensity); [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); @@ -98,8 +98,11 @@ protected override void LoadComplete() { base.LoadComplete(); + GraphTypeInternal.ValueChanged += _ => updateGraphVisibility(); + GraphTypeInternal.Value = GraphType.Value; + GraphTypeInternal.BindTo(GraphType); + Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true); - ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true); AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); @@ -109,17 +112,19 @@ protected override void LoadComplete() Width = previousWidth; } - protected override void UpdateObjects(IEnumerable objects) + protected override void UpdateTimeBounds() { - graph.Objects = objects; - info.StartTime = bar.StartTime = FirstHitTime; info.EndTime = bar.EndTime = LastHitTime; } + protected override void UpdateFromObjects(IEnumerable objects) => graph.SetFromObjects(objects); + + protected override void UpdateFromStrains(double[] sectionStrains) => graph.SetFromStrains(sectionStrains); + private void updateGraphVisibility() { - graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In); + graph.FadeTo(GraphTypeInternal.Value != DifficultyGraphType.None ? 1 : 0, 200, Easing.In); } protected override void Update() diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs index be570c1578a4..bb58fbc72346 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs @@ -10,49 +10,48 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Graphics.UserInterface; +using osu.Game.Utils; namespace osu.Game.Screens.Play.HUD { - public partial class ArgonSongProgressGraph : SegmentedGraph + public partial class ArgonSongProgressGraph : SegmentedGraph { private const int tier_count = 5; private const int display_granularity = 200; - private IEnumerable? objects; - - public IEnumerable Objects + public void SetFromObjects(IEnumerable objects) { - set - { - objects = value; + float[] values = new float[display_granularity]; - int[] values = new int[display_granularity]; + if (!objects.Any()) + return; - if (!objects.Any()) - return; + (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects); - (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects); + if (lastHit == 0) + lastHit = objects.Last().StartTime; - if (lastHit == 0) - lastHit = objects.Last().StartTime; + double interval = (lastHit - firstHit + 1) / display_granularity; - double interval = (lastHit - firstHit + 1) / display_granularity; + foreach (var h in objects) + { + double endTime = h.GetEndTime(); - foreach (var h in objects) - { - double endTime = h.GetEndTime(); + Debug.Assert(endTime >= h.StartTime); - Debug.Assert(endTime >= h.StartTime); + int startRange = (int)((h.StartTime - firstHit) / interval); + int endRange = (int)((endTime - firstHit) / interval); + for (int i = startRange; i <= endRange; i++) + values[i]++; + } - int startRange = (int)((h.StartTime - firstHit) / interval); - int endRange = (int)((endTime - firstHit) / interval); - for (int i = startRange; i <= endRange; i++) - values[i]++; - } + Values = values; + } - Values = values; - } + public void SetFromStrains(double[] strains) + { + Values = FormatUtils.ResampleStrains(strains, display_granularity).Select(value => (float)value).ToArray(); } public ArgonSongProgressGraph() diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 672017750d4e..a47cd1ee2d52 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -31,8 +31,8 @@ public partial class DefaultSongProgress : SongProgress private readonly SongProgressInfo info; private readonly Container content; - [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))] - public Bindable ShowGraph { get; } = new BindableBool(true); + [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.GraphType), nameof(SongProgressStrings.GraphTypeDescription))] + public Bindable GraphType { get; } = new Bindable(DifficultyGraphType.ObjectDensity); [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); @@ -95,24 +95,31 @@ private void load(OsuColour colours) protected override void LoadComplete() { + GraphTypeInternal.ValueChanged += _ => updateGraphVisibility(); + GraphTypeInternal.Value = GraphType.Value; + GraphTypeInternal.BindTo(GraphType); + Interactive.BindValueChanged(_ => updateBarVisibility(), true); - ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowTime.BindValueChanged(_ => updateTimeVisibility(), true); AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); + updateGraphVisibility(); + base.LoadComplete(); } - protected override void UpdateObjects(IEnumerable objects) + protected override void UpdateTimeBounds() { - graph.Objects = objects; - info.StartTime = FirstHitTime; info.EndTime = LastHitTime; bar.StartTime = FirstHitTime; bar.EndTime = LastHitTime; } + protected override void UpdateFromObjects(IEnumerable objects) => graph.SetFromObjects(objects); + + protected override void UpdateFromStrains(double[] sectionStrains) => graph.SetFromStrains(sectionStrains); + protected override void UpdateProgress(double progress, bool isIntro) { graph.Progress = isIntro ? 0 : (int)(graph.ColumnCount * progress); @@ -140,8 +147,8 @@ private void updateGraphVisibility() { float barHeight = bottom_bar_height + handle_size.Y; - bar.ResizeHeightTo(ShowGraph.Value ? barHeight + graph_height : barHeight, transition_duration, Easing.In); - graph.FadeTo(ShowGraph.Value ? 1 : 0, transition_duration, Easing.In); + bar.ResizeHeightTo(GraphTypeInternal.Value != DifficultyGraphType.None ? barHeight + graph_height : barHeight, transition_duration, Easing.In); + graph.FadeTo(GraphTypeInternal.Value != DifficultyGraphType.None ? 1 : 0, transition_duration, Easing.In); updateInfoMargin(); } @@ -155,7 +162,7 @@ private void updateTimeVisibility() private void updateInfoMargin() { - float finalMargin = bottom_bar_height + (Interactive.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0); + float finalMargin = bottom_bar_height + (Interactive.Value ? handle_size.Y : 0) + (GraphTypeInternal.Value != DifficultyGraphType.None ? graph_height : 0); info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In); } } diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs index 047c64a4a46d..50808e172ba7 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgressGraph.cs @@ -8,44 +8,44 @@ using System.Diagnostics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; +using osu.Game.Utils; namespace osu.Game.Screens.Play.HUD { public partial class DefaultSongProgressGraph : SquareGraph { - private IEnumerable objects; + private const int granularity = 200; - public IEnumerable Objects + public void SetFromObjects(IEnumerable objects) { - set - { - objects = value; - - const int granularity = 200; - Values = new int[granularity]; + Values = new float[granularity]; - if (!objects.Any()) - return; + if (!objects.Any()) + return; - (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects); + (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects); - if (lastHit == 0) - lastHit = objects.Last().StartTime; + if (lastHit == 0) + lastHit = objects.Last().StartTime; - double interval = (lastHit - firstHit + 1) / granularity; + double interval = (lastHit - firstHit + 1) / granularity; - foreach (var h in objects) - { - double endTime = h.GetEndTime(); + foreach (var h in objects) + { + double endTime = h.GetEndTime(); - Debug.Assert(endTime >= h.StartTime); + Debug.Assert(endTime >= h.StartTime); - int startRange = (int)((h.StartTime - firstHit) / interval); - int endRange = (int)((endTime - firstHit) / interval); - for (int i = startRange; i <= endRange; i++) - Values[i]++; - } + int startRange = (int)((h.StartTime - firstHit) / interval); + int endRange = (int)((endTime - firstHit) / interval); + for (int i = startRange; i <= endRange; i++) + Values[i]++; } } + + public void SetFromStrains(double[] strains) + { + Values = FormatUtils.ResampleStrains(strains, granularity).Select(value => (float)value).ToArray(); + } } } diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 296306ec8950..4c5bcd377b4e 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -3,18 +3,30 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { + public enum DifficultyGraphType + { + None, + ObjectDensity, + Difficulty + } + public abstract partial class SongProgress : OverlayContainer, ISerialisableDrawable { // Some implementations of this element allow seeking during gameplay playback. @@ -32,6 +44,11 @@ public abstract partial class SongProgress : OverlayContainer, ISerialisableDraw /// public readonly Bindable Interactive = new Bindable(); + /// + /// Type of the difficulty info used in graph. + /// + protected readonly Bindable GraphTypeInternal = new Bindable(DifficultyGraphType.None); + public bool UsesFixedAnchor { get; set; } [Resolved] @@ -46,20 +63,6 @@ public abstract partial class SongProgress : OverlayContainer, ISerialisableDraw /// protected IClock FrameStableClock => frameStableClock ?? GameplayClock; - private IEnumerable? objects; - - public IEnumerable Objects - { - set - { - objects = value; - - (FirstHitTime, LastHitTime) = BeatmapExtensions.CalculatePlayableBounds(objects); - - UpdateObjects(objects); - } - } - protected override void LoadComplete() { base.LoadComplete(); @@ -67,10 +70,6 @@ protected override void LoadComplete() Show(); } - protected double FirstHitTime { get; private set; } - - protected double LastHitTime { get; private set; } - /// /// Called every update frame with current progress information. /// @@ -78,8 +77,6 @@ protected override void LoadComplete() /// If true, progress is (0..1) through the intro. protected abstract void UpdateProgress(double progress, bool isIntro); - protected virtual void UpdateObjects(IEnumerable objects) { } - [BackgroundDependencyLoader] private void load(DrawableRuleset? drawableRuleset, Player? player) { @@ -90,6 +87,27 @@ private void load(DrawableRuleset? drawableRuleset, Player? player) Objects = drawableRuleset.Objects; } + + GraphTypeInternal.BindValueChanged(_ => updateBasedOnGraphType(), true); + } + + private void updateBasedOnGraphType() + { + switch (GraphTypeInternal.Value) + { + case DifficultyGraphType.None: + UpdateFromObjects(Enumerable.Empty()); + break; + + case DifficultyGraphType.ObjectDensity: + if (objects != null) UpdateFromObjects(objects); + break; + + case DifficultyGraphType.Difficulty: + if (sectionStrains != null) UpdateFromStrains(getTotalStrains(sectionStrains)); + else calculateStrains(); + break; + } } protected override void PopIn() => this.FadeIn(500, Easing.OutQuint); @@ -127,5 +145,79 @@ protected override void Update() UpdateProgress(objectOffsetCurrent / objectDuration, false); } } + + protected virtual void UpdateTimeBounds() { } + + #region object density + + protected double FirstHitTime { get; private set; } + + protected double LastHitTime { get; private set; } + + private IEnumerable? objects; + + public IEnumerable Objects + { + set + { + objects = value; + + (FirstHitTime, LastHitTime) = BeatmapExtensions.CalculatePlayableBounds(objects); + + UpdateTimeBounds(); + updateBasedOnGraphType(); + } + } + + protected virtual void UpdateFromObjects(IEnumerable objects) { } + + #endregion + + #region diffcalc + + private bool strainsCalculationWasStarted; + + private List? sectionStrains; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + private void calculateStrains() + { + // No need for another recalc if strains are being recalculated right now; + if (strainsCalculationWasStarted) return; + + strainsCalculationWasStarted = true; + difficultyCache.GetSectionDifficultiesAsync(beatmap.Value, ruleset.Value.CreateInstance(), mods.Value.ToArray()) + .ContinueWith(task => Schedule(() => + { + sectionStrains = task.GetResultSafely(); + updateBasedOnGraphType(); + }), TaskContinuationOptions.OnlyOnRanToCompletion); + } + + private double[] getTotalStrains(List allStrains) + { + var result = allStrains + .SelectMany(arr => arr.Select((value, index) => (value, index))) + .GroupBy(x => x.index) + .Select(g => Math.Sqrt(g.Sum(x => x.value * x.value))); + + // Skip one to account for delay in strains + return result.Skip(1).ToArray(); + } + + protected virtual void UpdateFromStrains(double[] sectionStrains) { } + + #endregion } } diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index 0c7b485755f4..40ce5af2070d 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -43,9 +43,9 @@ public int Progress private float[] calculatedValues = Array.Empty(); // values but adjusted to fit the amount of columns - private int[] values; + private float[] values; - public int[] Values + public float[] Values { get => values; set @@ -168,13 +168,13 @@ private void recalculateValues() return; } - int max = values.Max(); + float max = values.Max(); float step = values.Length / (float)ColumnCount; for (float i = 0; i < values.Length; i += step) { - newValues.Add((float)values[(int)i] / max); + newValues.Add(values[(int)i] / max); } calculatedValues = newValues.ToArray(); diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index cccad3711cc5..798b16bf34d9 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using Humanizer; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; @@ -59,5 +60,160 @@ public static int FindPrecision(decimal d) /// The base BPM to round. /// Rate adjustment, if applicable. public static int RoundBPM(double baseBpm, double rate = 1) => (int)Math.Round(Math.Round(baseBpm) * rate); + + /// + /// Resampling strain values to certain bin size. + /// + /// + /// The main feature of this resampling is that peak strains will be always preserved. + /// This means that the highest strain can't be decreased by averaging or interpolation. + /// + public static double[] ResampleStrains(double[] values, int targetSize) + { + // Set to at least one value, what will be 0 in this case + if (values.Length == 0) + values = new double[1]; + + if (targetSize > values.Length) + return resamplingUpscale(values, targetSize); + else if (targetSize < values.Length) + return resamplingDownscale(values, targetSize); + + return (double[])values.Clone(); + } + + private static double[] resamplingUpscale(double[] values, int targetSize) + { + // Create array filled with -inf + double[] result = Enumerable.Repeat(double.NegativeInfinity, targetSize).ToArray(); + + // First and last peaks are constant + result[0] = values[0]; + result[^1] = values[^1]; + + // On the first pass we place peaks + + int sourceIndex = 1; + int targetIndex = 1; + + // Adjust sizes accounting for the fact that first and last elements already set-up + int sourceSize = Math.Max(1, values.Length - 1); + targetSize -= 1; + + for (; targetIndex < targetSize - 1; targetIndex++) + { + double sourceProgress = (double)sourceIndex / sourceSize; + + double targetProgressNext = (targetIndex + 1.0) / targetSize; + + // If we reached the point where source is between current and next - then peak is either current or next + if (sourceProgress <= targetProgressNext) + { + double targetProgressCurrent = (double)targetIndex / targetSize; + + double distanceToCurrent = sourceProgress - targetProgressCurrent; + double distanceToNext = targetProgressNext - sourceProgress; + + // If it's next what is closer - abbadon current and move to next immediatly + if (distanceToNext < distanceToCurrent) + { + result[targetIndex] = double.NegativeInfinity; + targetIndex++; + } + + result[targetIndex] = values[sourceIndex]; + sourceIndex++; + } + } + + // On second pass we interpolate between peaks + + sourceIndex = 0; + targetIndex = 1; + + for (; targetIndex < targetSize; targetIndex++) + { + // If we're on peak - skip iteration + if (result[targetIndex] != double.NegativeInfinity) + { + sourceIndex++; + continue; + } + + double targetProgress = (double)targetIndex / targetSize; + + double previousPeakProgress = (double)sourceIndex / sourceSize; + double nextPeakProgress = (sourceIndex + 1.0) / sourceSize; + + double distanceToPreviousPeak = targetProgress - previousPeakProgress; + double distanceToNextPeak = nextPeakProgress - targetProgress; + + double lerpCoef = distanceToPreviousPeak / (distanceToPreviousPeak + distanceToNextPeak); + double nextValue = sourceIndex + 1 < values.Length ? values[sourceIndex + 1] : values[sourceIndex]; + result[targetIndex] = double.Lerp(values[sourceIndex], nextValue, lerpCoef); + } + + return result; + } + + private static double[] resamplingDownscale(double[] values, int targetSize) + { + double[] result = new double[targetSize]; + + int sourceIndex = 0; + int targetIndex = 0; + + double currentSampleMax = double.NegativeInfinity; + + for (; sourceIndex < values.Length; sourceIndex++) + { + double currentValue = values[sourceIndex]; + + double sourceProgress = (sourceIndex + 0.5) / values.Length; + double targetProgressBorder = (targetIndex + 1.0) / targetSize; + + double distanceToBorder = targetProgressBorder - sourceProgress; + + // Handle transition to next sample + if (distanceToBorder < 0) + { + double targetProgressCurrent = (targetIndex + 0.5) / targetSize; + double targetProgressNext = (targetIndex + 1.5) / targetSize; + + // Try fit weighted current into still current sample + // It would always be closer to Next than to Current + double weight = (targetProgressNext - sourceProgress) / (sourceProgress - targetProgressCurrent); + double weightedValue = currentValue * weight; + + if (currentSampleMax < weightedValue) currentSampleMax = weightedValue; + + // Flush current max + result[targetIndex] = currentSampleMax; + targetIndex++; + currentSampleMax = double.NegativeInfinity; + + // Try to fit weighted previous into future sample + if (sourceIndex > 0) + { + double prevValue = values[sourceIndex - 1]; + double sourceProgressPrev = (sourceIndex - 0.5) / values.Length; + + // It would always be closer to Current than to Current + weight = (sourceProgressPrev - targetProgressCurrent) / (targetProgressNext - sourceProgressPrev); + weightedValue = prevValue * weight; + + currentSampleMax = weightedValue; + } + } + + // Replace with maximum of the sample + if (currentSampleMax < currentValue) currentSampleMax = currentValue; + } + + // Flush last value + result[targetIndex] = currentSampleMax; + + return result; + } } }