1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 02:49:53 +08:00
Files
osu-lazer/osu.Game/Beatmaps/IBeatmap.cs
T
Bartłomiej Dach 91f3be5fea Move BeatmapVersion from BeatmapInfo to IBeatmap
Closes https://github.com/ppy/osu/issues/32420.

The failure cause here is that in editor the beatmap version for the
beatmap affected (or... any beatmap, really), is 0 (ZERO). That is
probably a regression from https://github.com/ppy/osu/pull/32315, but
like... can we universally agree that calling that change "a regression"
in any capacity is dumb? Like what was that code *doing* playing dumb
reference games and copying stuff into an arbitrary instance that could
get or not get used later on? And now you have a 50/50 chance of
accessing the *correct* model's field, depending on whether you go via
`BeatmapInfo` or `Beatmap.BeatmapInfo`?

Moving the field to `IBeatmap`, i.e. what is by now - by consensus,
since https://github.com/ppy/osu/pull/28473 - supposed to be the "decoded
and materialised" beatmap, fixes this issue.

I probably should have done this as part of
https://github.com/ppy/osu/pull/28473 but it slipped my mind. Probably
for the better too because this change has rather large chances of
breaking stuff so maybe better to examine it in isolation (via diffcalc
runs or whatever).

For added humour points, you'd say that the field on `BeatmapInfo` was
not `[Ignore]`d, so this is a realm schema change, right? No. As far as
I can tell, it's not. I opened realm studio and `BeatmapVersion` *is not
a listed column` on `Beatmap` models.

I'm also not gonna get into the fact that I think `EditorBeatmap` doing
dumb games with juggling two `BeatmapInfo` references since
https://github.com/ppy/osu/pull/15075 is bad, because I don't think I
have the mental capacity to hotfix this by going down that train of
thought.
2025-03-17 15:04:48 +01:00

217 lines
7.9 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Lists;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A materialised beatmap.
/// Generally this interface will be implemented alongside <see cref="IBeatmap{T}"/>, which exposes the ruleset-typed hit objects.
/// </summary>
public interface IBeatmap
{
/// <summary>
/// This beatmap's info.
/// </summary>
BeatmapInfo BeatmapInfo { get; set; }
/// <summary>
/// This beatmap's metadata.
/// </summary>
BeatmapMetadata Metadata { get; }
/// <summary>
/// This beatmap's difficulty settings.
/// </summary>
public BeatmapDifficulty Difficulty { get; set; }
/// <summary>
/// The control points in this beatmap.
/// </summary>
ControlPointInfo ControlPointInfo { get; set; }
/// <summary>
/// The breaks in this beatmap.
/// </summary>
SortedList<BreakPeriod> Breaks { get; set; }
/// <summary>
/// All lines from the [Events] section which aren't handled in the encoding process yet.
/// These lines should be written out to the beatmap file on save or export.
/// </summary>
List<string> UnhandledEventLines { get; }
/// <summary>
/// Total amount of break time in the beatmap.
/// </summary>
double TotalBreakTime { get; }
/// <summary>
/// The hitobjects contained by this beatmap.
/// </summary>
IReadOnlyList<HitObject> HitObjects { get; }
/// <summary>
/// Returns statistics for the <see cref="HitObjects"/> contained in this beatmap.
/// </summary>
IEnumerable<BeatmapStatistic> GetStatistics();
/// <summary>
/// Finds the most common beat length represented by the control points in this beatmap.
/// </summary>
double GetMostCommonBeatLength();
double AudioLeadIn { get; internal set; }
float StackLeniency { get; internal set; }
bool SpecialStyle { get; internal set; }
bool LetterboxInBreaks { get; internal set; }
bool WidescreenStoryboard { get; internal set; }
bool EpilepsyWarning { get; internal set; }
bool SamplesMatchPlaybackRate { get; internal set; }
/// <summary>
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
/// </summary>
/// <remarks>
/// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap
/// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider.
///
/// This is only a hint property, used by the editor in <see cref="IDistanceSnapProvider"/> implementations. It does not directly affect the beatmap or gameplay.
/// </remarks>
double DistanceSpacing { get; internal set; }
int GridSize { get; internal set; }
double TimelineZoom { get; internal set; }
CountdownType Countdown { get; internal set; }
/// <summary>
/// The number of beats to move the countdown backwards (compared to its default location).
/// </summary>
int CountdownOffset { get; internal set; }
int[] Bookmarks { get; internal set; }
int BeatmapVersion { get; }
/// <summary>
/// Creates a shallow-clone of this beatmap and returns it.
/// </summary>
/// <returns>The shallow-cloned beatmap.</returns>
IBeatmap Clone();
}
/// <summary>
/// A materialised beatmap containing converted HitObjects.
/// </summary>
public interface IBeatmap<out T> : IBeatmap
where T : HitObject
{
/// <summary>
/// The hitobjects contained by this beatmap.
/// </summary>
new IReadOnlyList<T> HitObjects { get; }
}
public static class BeatmapExtensions
{
/// <summary>
/// Finds the maximum achievable combo by hitting all <see cref="HitObject"/>s in a beatmap.
/// </summary>
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);
}
}
/// <summary>
/// Find the total milliseconds between the first and last hittable objects.
/// </summary>
/// <remarks>
/// This is cached to <see cref="BeatmapInfo.Length"/>, so using that is preferable when available.
/// </remarks>
public static double CalculatePlayableLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects);
/// <summary>
/// Find the total milliseconds between the first and last hittable objects, excluding any break time.
/// </summary>
public static double CalculateDrainLength(this IBeatmap beatmap) => Math.Max(CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime, 0);
/// <summary>
/// Find the timestamps in milliseconds of the start and end of the playable region.
/// </summary>
public static (double start, double end) CalculatePlayableBounds(this IBeatmap beatmap) => CalculatePlayableBounds(beatmap.HitObjects);
/// <summary>
/// Find the absolute end time of the latest <see cref="HitObject"/> in a beatmap. Will throw if beatmap contains no objects.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <exception cref="InvalidOperationException">If <paramref name="beatmap"/> has no objects.</exception>
public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime());
#region Helper methods
/// <summary>
/// Find the total milliseconds between the first and last hittable objects.
/// </summary>
/// <remarks>
/// This is cached to <see cref="BeatmapInfo.Length"/>, so using that is preferable when available.
/// </remarks>
public static double CalculatePlayableLength(IEnumerable<HitObject> objects)
{
(double start, double end) = CalculatePlayableBounds(objects);
return end - start;
}
/// <summary>
/// Find the timestamps in milliseconds of the start and end of the playable region.
/// </summary>
public static (double start, double end) CalculatePlayableBounds(IEnumerable<HitObject> objects)
{
if (!objects.Any())
return (0, 0);
double lastObjectTime = objects.Max(o => o.GetEndTime());
double firstObjectTime = objects.First().StartTime;
return (firstObjectTime, lastObjectTime);
}
#endregion
}
}