diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index be2006e67a..410fd5e92e 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -48,6 +48,44 @@ 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 beatLengthsAndDurations = + // 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 => t.beatLength) + .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) + // And if there are no timing points, use a default. + .DefaultIfEmpty((TimingControlPoint.DEFAULT_BEAT_LENGTH, 0)); + + // Find the single beat length with the maximum aggregate duration. + double maxDurationBeatLength = double.NegativeInfinity; + double maxDuration = double.NegativeInfinity; + + foreach (var (beatLength, duration) in beatLengthsAndDurations) + { + if (duration > maxDuration) + { + maxDuration = duration; + maxDurationBeatLength = beatLength; + } + } + + return 60000 / maxDurationBeatLength; + } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 42418e532b..0d4cc38ac3 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 = 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 8f27e0b0e9..aaca8f7658 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 165d2ba278..0e9008ba68 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -84,6 +84,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 64894544f4..53c1360bfa 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -39,6 +39,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..abcd697d85 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 {beatmap.GetMostCommonBeatLength():0})"; } private OsuSpriteText[] getMapper(BeatmapMetadata metadata)