diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 5435e86dfd..e5b6a4bc44 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -1,6 +1,7 @@ // 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 osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; using System.Collections.Generic; @@ -48,6 +49,31 @@ namespace osu.Game.Beatmaps public virtual IEnumerable GetStatistics() => Enumerable.Empty(); + public double GetMostCommonBeatLength() + { + // The last playable time in the beatmap - the last timing point extends to this time. + // Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context. + double lastTime = HitObjects.LastOrDefault()?.GetEndTime() ?? ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0; + + var mostCommon = + // Construct a set of (beatLength, duration) tuples for each individual timing point. + ControlPointInfo.TimingPoints.Select((t, i) => + { + if (t.Time > lastTime) + return (beatLength: t.BeatLength, 0); + + var nextTime = i == ControlPointInfo.TimingPoints.Count - 1 ? lastTime : ControlPointInfo.TimingPoints[i + 1].Time; + return (beatLength: t.BeatLength, duration: nextTime - t.Time); + }) + // Aggregate durations into a set of (beatLength, duration) tuples for each beat length + .GroupBy(t => Math.Round(t.beatLength * 1000) / 1000) + .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) + // Get the most common one, or 0 as a suitable default + .OrderByDescending(i => i.duration).FirstOrDefault(); + + return mostCommon.beatLength; + } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 42418e532b..b934ac556d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -451,7 +451,7 @@ namespace osu.Game.Beatmaps // TODO: this should be done in a better place once we actually need to dynamically update it. beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = beatmap.ControlPointInfo.BPMMode; + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); beatmapInfos.Add(beatmap.BeatmapInfo); } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index e8a91e4001..5cc60a5758 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -101,13 +101,6 @@ namespace osu.Game.Beatmaps.ControlPoints public double BPMMinimum => 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; - /// - /// Finds the mode BPM (most common BPM) represented by the control points. - /// - [JsonIgnore] - public double BPMMode => - 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; - /// /// Remove all s and return to a pristine state. /// diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 7dd85e1232..9847ea020a 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -47,6 +47,11 @@ namespace osu.Game.Beatmaps /// IEnumerable GetStatistics(); + /// + /// Finds the most common beat length represented by the control points in this beatmap. + /// + double GetMostCommonBeatLength(); + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index a54a95f59d..174ff1478b 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -88,6 +88,8 @@ namespace osu.Game.Screens.Edit public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); + public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 565595656f..74fbe540fa 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -43,6 +43,8 @@ namespace osu.Game.Screens.Play public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); + public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); + public IBeatmap Clone() => PlayableBeatmap.Clone(); private readonly Bindable lastJudgementResult = new Bindable(); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 04c1f6efe4..86cb561bc7 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -391,7 +391,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(bpmMin, bpmMax)) return $"{bpmMin:0}"; - return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.ControlPointInfo.BPMMode:0})"; + return $"{bpmMin:0}-{bpmMax:0} (mostly {60000 / beatmap.GetMostCommonBeatLength():0})"; } private OsuSpriteText[] getMapper(BeatmapMetadata metadata)