// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Beatmaps { /// /// A materialised beatmap. /// Generally this interface will be implemented alongside , which exposes the ruleset-typed hit objects. /// public interface IBeatmap { /// /// This beatmap's info. /// BeatmapInfo BeatmapInfo { get; set; } /// /// This beatmap's metadata. /// BeatmapMetadata Metadata { get; } /// /// This beatmap's difficulty settings. /// public BeatmapDifficulty Difficulty { get; set; } /// /// The control points in this beatmap. /// ControlPointInfo ControlPointInfo { get; set; } /// /// The breaks in this beatmap. /// List Breaks { get; } /// /// Total amount of break time in the beatmap. /// double TotalBreakTime { get; } /// /// The hitobjects contained by this beatmap. /// IReadOnlyList HitObjects { get; } /// /// Returns statistics for the contained in this beatmap. /// 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. /// /// The shallow-cloned beatmap. IBeatmap Clone(); } /// /// A materialised beatmap containing converted HitObjects. /// public interface IBeatmap : IBeatmap where T : HitObject { /// /// The hitobjects contained by this beatmap. /// new IReadOnlyList HitObjects { get; } } public static class BeatmapExtensions { /// /// Finds the maximum achievable combo by hitting all s in a beatmap. /// public static int GetMaxCombo(this IBeatmap beatmap) { int combo = 0; foreach (var h in beatmap.HitObjects) addCombo(h, ref combo); return combo; static void addCombo(HitObject hitObject, ref int combo) { if (hitObject.Judgement.MaxResult.AffectsCombo()) combo++; foreach (var nested in hitObject.NestedHitObjects) addCombo(nested, ref combo); } } /// /// Find the total milliseconds between the first and last hittable objects. /// /// /// This is cached to , so using that is preferable when available. /// public static double CalculatePlayableLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects); /// /// Find the total milliseconds between the first and last hittable objects, excluding any break time. /// public static double CalculateDrainLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime; /// /// Find the timestamps in milliseconds of the start and end of the playable region. /// public static (double start, double end) CalculatePlayableBounds(this IBeatmap beatmap) => CalculatePlayableBounds(beatmap.HitObjects); /// /// Find the absolute end time of the latest in a beatmap. Will throw if beatmap contains no objects. /// /// /// This correctly accounts for rulesets which have concurrent hitobjects which may have durations, causing the .Last() object /// to not necessarily have the latest end time. /// /// It's not super efficient so calls should be kept to a minimum. /// public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime()); #region Helper methods /// /// Find the total milliseconds between the first and last hittable objects. /// /// /// This is cached to , so using that is preferable when available. /// public static double CalculatePlayableLength(IEnumerable objects) { (double start, double end) = CalculatePlayableBounds(objects); return end - start; } /// /// Find the timestamps in milliseconds of the start and end of the playable region. /// public static (double start, double end) CalculatePlayableBounds(IEnumerable objects) { if (!objects.Any()) return (0, 0); double lastObjectTime = objects.Max(o => o.GetEndTime()); double firstObjectTime = objects.First().StartTime; return (firstObjectTime, lastObjectTime); } #endregion } }