mirror of
https://github.com/osukey/osukey.git
synced 2025-08-03 22:56:36 +09:00
Merge branch 'master' into mbd-beatmap-set-cover
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
// 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 osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using System.Collections.Generic;
|
||||
@ -48,17 +49,34 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public virtual IEnumerable<BeatmapStatistic> GetStatistics() => Enumerable.Empty<BeatmapStatistic>();
|
||||
|
||||
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<T> Clone()
|
||||
{
|
||||
var clone = (Beatmap<T>)MemberwiseClone();
|
||||
|
||||
clone.ControlPointInfo = ControlPointInfo.CreateCopy();
|
||||
// todo: deep clone other elements as required.
|
||||
|
||||
return clone;
|
||||
}
|
||||
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
|
||||
}
|
||||
|
||||
public class Beatmap : Beatmap<HitObject>
|
||||
|
@ -17,18 +17,16 @@ namespace osu.Game.Beatmaps
|
||||
public abstract class BeatmapConverter<T> : IBeatmapConverter
|
||||
where T : HitObject
|
||||
{
|
||||
private event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
||||
private event Action<HitObject, IEnumerable<HitObject>> objectConverted;
|
||||
|
||||
event Action<HitObject, IEnumerable<HitObject>> IBeatmapConverter.ObjectConverted
|
||||
{
|
||||
add => ObjectConverted += value;
|
||||
remove => ObjectConverted -= value;
|
||||
add => objectConverted += value;
|
||||
remove => objectConverted -= value;
|
||||
}
|
||||
|
||||
public IBeatmap Beatmap { get; }
|
||||
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
@ -41,8 +39,6 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public IBeatmap Convert(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.cancellationToken = cancellationToken;
|
||||
|
||||
// We always operate on a clone of the original beatmap, to not modify it game-wide
|
||||
return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
|
||||
}
|
||||
@ -55,19 +51,6 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>The converted Beatmap.</returns>
|
||||
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
#pragma warning disable 618
|
||||
return ConvertBeatmap(original);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the conversion of a Beatmap using this Beatmap Converter.
|
||||
/// </summary>
|
||||
/// <param name="original">The un-converted Beatmap.</param>
|
||||
/// <returns>The converted Beatmap.</returns>
|
||||
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
|
||||
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original)
|
||||
{
|
||||
var beatmap = CreateBeatmap();
|
||||
|
||||
beatmap.BeatmapInfo = original.BeatmapInfo;
|
||||
@ -92,10 +75,10 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
var converted = ConvertHitObject(obj, beatmap, cancellationToken);
|
||||
|
||||
if (ObjectConverted != null)
|
||||
if (objectConverted != null)
|
||||
{
|
||||
converted = converted.ToList();
|
||||
ObjectConverted.Invoke(obj, converted);
|
||||
objectConverted.Invoke(obj, converted);
|
||||
}
|
||||
|
||||
foreach (var c in converted)
|
||||
@ -121,21 +104,6 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="beatmap">The un-converted Beatmap.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The converted hit object.</returns>
|
||||
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
#pragma warning disable 618
|
||||
return ConvertHitObject(original, beatmap);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the conversion of a hit object.
|
||||
/// This method is generally executed sequentially for all objects in a beatmap.
|
||||
/// </summary>
|
||||
/// <param name="original">The hit object to convert.</param>
|
||||
/// <param name="beatmap">The un-converted Beatmap.</param>
|
||||
/// <returns>The converted hit object.</returns>
|
||||
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
|
||||
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap) => Enumerable.Empty<T>();
|
||||
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) => Enumerable.Empty<T>();
|
||||
}
|
||||
}
|
||||
|
@ -69,8 +69,8 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
|
||||
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
|
||||
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
|
||||
public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
|
||||
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state (but not during updates to ruleset and mods if a stale value is already propagated).</returns>
|
||||
public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
|
||||
|
||||
@ -90,9 +90,9 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param>
|
||||
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
|
||||
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
|
||||
public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
|
||||
CancellationToken cancellationToken = default)
|
||||
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state.</returns>
|
||||
public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
@ -313,7 +313,7 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
private class BindableStarDifficulty : Bindable<StarDifficulty>
|
||||
private class BindableStarDifficulty : Bindable<StarDifficulty?>
|
||||
{
|
||||
public readonly BeatmapInfo Beatmap;
|
||||
public readonly CancellationToken CancellationToken;
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
|
||||
|
||||
return $"{Metadata} {version}".Trim();
|
||||
return $"{Metadata ?? BeatmapSet?.Metadata} {version}".Trim();
|
||||
}
|
||||
|
||||
public bool Equals(BeatmapInfo other)
|
||||
|
@ -20,6 +20,7 @@ using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Database;
|
||||
@ -64,7 +65,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
|
||||
protected override string ImportFromStablePath => "Songs";
|
||||
protected override string ImportFromStablePath => ".";
|
||||
|
||||
protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage();
|
||||
|
||||
private readonly RulesetStore rulesets;
|
||||
private readonly BeatmapStore beatmaps;
|
||||
@ -110,8 +113,6 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
var metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "artist",
|
||||
Title = "title",
|
||||
Author = user,
|
||||
};
|
||||
|
||||
@ -125,7 +126,6 @@ namespace osu.Game.Beatmaps
|
||||
BaseDifficulty = new BeatmapDifficulty(),
|
||||
Ruleset = ruleset,
|
||||
Metadata = metadata,
|
||||
Version = "difficulty"
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -153,7 +153,7 @@ namespace osu.Game.Beatmaps
|
||||
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
|
||||
|
||||
if (onlineLookupQueue != null)
|
||||
await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken);
|
||||
await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
||||
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
|
||||
@ -309,6 +309,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
|
||||
|
||||
// best effort; may be higher than expected.
|
||||
GlobalStatistics.Get<int>(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count();
|
||||
|
||||
return working;
|
||||
}
|
||||
}
|
||||
@ -451,7 +454,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);
|
||||
}
|
||||
@ -523,6 +526,7 @@ namespace osu.Game.Beatmaps
|
||||
protected override IBeatmap GetBeatmap() => beatmap;
|
||||
protected override Texture GetBackground() => null;
|
||||
protected override Track GetBeatmapTrack() => null;
|
||||
public override Stream GetStream(string storagePath) => null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.IO.Network;
|
||||
@ -154,20 +153,31 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online")))
|
||||
{
|
||||
var found = db.QuerySingleOrDefault<CachedOnlineBeatmapLookup>(
|
||||
"SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap);
|
||||
db.Open();
|
||||
|
||||
if (found != null)
|
||||
using (var cmd = db.CreateCommand())
|
||||
{
|
||||
var status = (BeatmapSetOnlineStatus)found.approved;
|
||||
cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path";
|
||||
|
||||
beatmap.Status = status;
|
||||
beatmap.BeatmapSet.Status = status;
|
||||
beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id;
|
||||
beatmap.OnlineBeatmapID = found.beatmap_id;
|
||||
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value));
|
||||
cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path));
|
||||
|
||||
LogForModel(set, $"Cached local retrieval for {beatmap}.");
|
||||
return true;
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
var status = (BeatmapSetOnlineStatus)reader.GetByte(2);
|
||||
|
||||
beatmap.Status = status;
|
||||
beatmap.BeatmapSet.Status = status;
|
||||
beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0);
|
||||
beatmap.OnlineBeatmapID = reader.GetInt32(1);
|
||||
|
||||
LogForModel(set, $"Cached local retrieval for {beatmap}.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Logging;
|
||||
@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
try
|
||||
{
|
||||
using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path))))
|
||||
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
|
||||
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -46,8 +46,6 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
private string getPathForFile(string filename) => BeatmapSetInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath;
|
||||
|
||||
protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes.
|
||||
|
||||
protected override Texture GetBackground()
|
||||
@ -57,7 +55,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
try
|
||||
{
|
||||
return resources.LargeTextureStore.Get(getPathForFile(Metadata.BackgroundFile));
|
||||
return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -73,7 +71,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
try
|
||||
{
|
||||
return resources.Tracks.Get(getPathForFile(Metadata.AudioFile));
|
||||
return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -89,7 +87,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
try
|
||||
{
|
||||
var trackData = resources.Files.GetStream(getPathForFile(Metadata.AudioFile));
|
||||
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
|
||||
return trackData == null ? null : new Waveform(trackData);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -105,7 +103,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
try
|
||||
{
|
||||
using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path))))
|
||||
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
|
||||
{
|
||||
var decoder = Decoder.GetDecoder<Storyboard>(stream);
|
||||
|
||||
@ -114,7 +112,7 @@ namespace osu.Game.Beatmaps
|
||||
storyboard = decoder.Decode(stream);
|
||||
else
|
||||
{
|
||||
using (var secondaryStream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile))))
|
||||
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile))))
|
||||
storyboard = decoder.Decode(stream, secondaryStream);
|
||||
}
|
||||
}
|
||||
@ -142,6 +140,8 @@ namespace osu.Game.Beatmaps
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,13 @@ namespace osu.Game.Beatmaps
|
||||
public int ID { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty("title_unicode")]
|
||||
public string TitleUnicode { get; set; }
|
||||
|
||||
public string Artist { get; set; }
|
||||
|
||||
[JsonProperty("artist_unicode")]
|
||||
public string ArtistUnicode { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
@ -51,7 +56,12 @@ namespace osu.Game.Beatmaps
|
||||
[JsonProperty(@"tags")]
|
||||
public string Tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time in milliseconds to begin playing the track for preview purposes.
|
||||
/// If -1, the track should begin playing at 40% of its length.
|
||||
/// </summary>
|
||||
public int PreviewTime { get; set; }
|
||||
|
||||
public string AudioFile { get; set; }
|
||||
public string BackgroundFile { get; set; }
|
||||
|
||||
|
@ -59,6 +59,13 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
|
||||
/// The path returned is relative to the user file storage.
|
||||
/// </summary>
|
||||
/// <param name="filename">The name of the file to get the storage path of.</param>
|
||||
public string GetPathForFile(string filename) => Files?.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath;
|
||||
|
||||
public List<BeatmapSetFileInfo> Files { get; set; }
|
||||
|
||||
public override string ToString() => Metadata?.ToString() ?? base.ToString();
|
||||
|
@ -3,16 +3,11 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public class BeatmapStatistic
|
||||
{
|
||||
[Obsolete("Use CreateIcon instead")] // can be removed 20210203
|
||||
public IconUsage Icon = FontAwesome.Regular.QuestionCircle;
|
||||
|
||||
/// <summary>
|
||||
/// A function to create the icon for display purposes. Use default icons available via <see cref="BeatmapStatisticIcon"/> whenever possible for conformity.
|
||||
/// </summary>
|
||||
@ -20,12 +15,5 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public string Content;
|
||||
public string Name;
|
||||
|
||||
public BeatmapStatistic()
|
||||
{
|
||||
#pragma warning disable 618
|
||||
CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.7f) };
|
||||
#pragma warning restore 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,13 +101,6 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
public double BPMMinimum =>
|
||||
60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the mode BPM (most common BPM) represented by the control points.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double BPMMode =>
|
||||
60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
|
||||
|
||||
/// <summary>
|
||||
/// Remove all <see cref="ControlPointGroup"/>s and return to a pristine state.
|
||||
/// </summary>
|
||||
|
@ -19,13 +19,13 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.1,
|
||||
Precision = 0.01,
|
||||
Default = 1,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark;
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier at this control point.
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
private const double default_beat_length = 60000.0 / 60.0;
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.YellowDark;
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1;
|
||||
|
||||
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
|
||||
{
|
||||
|
@ -151,7 +151,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
this.mods = mods;
|
||||
}
|
||||
|
||||
private IBindable<StarDifficulty> localStarDifficulty;
|
||||
private IBindable<StarDifficulty?> localStarDifficulty;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -160,7 +160,11 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
localStarDifficulty = ruleset != null
|
||||
? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token)
|
||||
: difficultyCache.GetBindableDifficulty(beatmap, difficultyCancellation.Token);
|
||||
localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue);
|
||||
localStarDifficulty.BindValueChanged(d =>
|
||||
{
|
||||
if (d.NewValue is StarDifficulty diff)
|
||||
StarDifficulty.Value = diff;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -34,8 +34,8 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
/// </summary>
|
||||
protected virtual double UnloadDelay => 10000;
|
||||
|
||||
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
|
||||
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay);
|
||||
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad) =>
|
||||
new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay) { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
protected override double TransformDuration => 400;
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Audio;
|
||||
@ -48,6 +49,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
protected override Track GetBeatmapTrack() => GetVirtualTrack();
|
||||
|
||||
public override Stream GetStream(string storagePath) => null;
|
||||
|
||||
private class DummyRulesetInfo : RulesetInfo
|
||||
{
|
||||
public override Ruleset CreateInstance() => new DummyRuleset();
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
@ -66,16 +67,14 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
protected override void ParseLine(Beatmap beatmap, Section section, string line)
|
||||
{
|
||||
var strippedLine = StripComments(line);
|
||||
|
||||
switch (section)
|
||||
{
|
||||
case Section.General:
|
||||
handleGeneral(strippedLine);
|
||||
handleGeneral(line);
|
||||
return;
|
||||
|
||||
case Section.Editor:
|
||||
handleEditor(strippedLine);
|
||||
handleEditor(line);
|
||||
return;
|
||||
|
||||
case Section.Metadata:
|
||||
@ -83,19 +82,19 @@ namespace osu.Game.Beatmaps.Formats
|
||||
return;
|
||||
|
||||
case Section.Difficulty:
|
||||
handleDifficulty(strippedLine);
|
||||
handleDifficulty(line);
|
||||
return;
|
||||
|
||||
case Section.Events:
|
||||
handleEvent(strippedLine);
|
||||
handleEvent(line);
|
||||
return;
|
||||
|
||||
case Section.TimingPoints:
|
||||
handleTimingPoint(strippedLine);
|
||||
handleTimingPoint(line);
|
||||
return;
|
||||
|
||||
case Section.HitObjects:
|
||||
handleHitObject(strippedLine);
|
||||
handleHitObject(line);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -348,8 +347,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (split.Length >= 8)
|
||||
{
|
||||
LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]);
|
||||
kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai);
|
||||
omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine);
|
||||
kiaiMode = effectFlags.HasFlagFast(LegacyEffectFlags.Kiai);
|
||||
omitFirstBarSignature = effectFlags.HasFlagFast(LegacyEffectFlags.OmitFirstBarLine);
|
||||
}
|
||||
|
||||
string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
|
||||
|
@ -273,7 +273,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (hitObject is IHasPath path)
|
||||
{
|
||||
addPathData(writer, path, position);
|
||||
writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true));
|
||||
writer.Write(getSampleBank(hitObject.Samples));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -329,7 +329,26 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
if (point.Type.Value != null)
|
||||
{
|
||||
if (point.Type.Value != lastType)
|
||||
// We've reached a new (explicit) segment!
|
||||
|
||||
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
|
||||
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
|
||||
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
|
||||
bool needsExplicitSegment = point.Type.Value != lastType || point.Type.Value == PathType.PerfectCurve;
|
||||
|
||||
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
|
||||
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
|
||||
if (i > 1)
|
||||
{
|
||||
// We need to use the absolute control point position to determine equality, otherwise floating point issues may arise.
|
||||
Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position.Value;
|
||||
Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position.Value;
|
||||
|
||||
if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y)
|
||||
needsExplicitSegment = true;
|
||||
}
|
||||
|
||||
if (needsExplicitSegment)
|
||||
{
|
||||
switch (point.Type.Value)
|
||||
{
|
||||
@ -401,15 +420,15 @@ namespace osu.Game.Beatmaps.Formats
|
||||
writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}"));
|
||||
}
|
||||
|
||||
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false, bool zeroBanks = false)
|
||||
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false)
|
||||
{
|
||||
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
|
||||
LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)normalBank)}:"));
|
||||
sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)addBank)}"));
|
||||
sb.Append(FormattableString.Invariant($"{(int)normalBank}:"));
|
||||
sb.Append(FormattableString.Invariant($"{(int)addBank}"));
|
||||
|
||||
if (!banksOnly)
|
||||
{
|
||||
@ -471,9 +490,6 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
|
||||
{
|
||||
if (hitSampleInfo == null)
|
||||
return "0";
|
||||
|
||||
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
|
||||
return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
|
@ -36,6 +36,14 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (ShouldSkipLine(line))
|
||||
continue;
|
||||
|
||||
if (section != Section.Metadata)
|
||||
{
|
||||
// comments should not be stripped from metadata lines, as the song metadata may contain "//" as valid data.
|
||||
line = StripComments(line);
|
||||
}
|
||||
|
||||
line = line.TrimEnd();
|
||||
|
||||
if (line.StartsWith('[') && line.EndsWith(']'))
|
||||
{
|
||||
if (!Enum.TryParse(line[1..^1], out section))
|
||||
@ -71,8 +79,6 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
protected virtual void ParseLine(T output, Section section, string line)
|
||||
{
|
||||
line = StripComments(line);
|
||||
|
||||
switch (section)
|
||||
{
|
||||
case Section.Colours:
|
||||
|
@ -45,8 +45,6 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
protected override void ParseLine(Storyboard storyboard, Section section, string line)
|
||||
{
|
||||
line = StripComments(line);
|
||||
|
||||
switch (section)
|
||||
{
|
||||
case Section.General:
|
||||
@ -139,7 +137,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
// this is random as hell but taken straight from osu-stable.
|
||||
frameDelay = Math.Round(0.015 * frameDelay) * 1.186 * (1000 / 60f);
|
||||
|
||||
var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever;
|
||||
var loopType = split.Length > 8 ? parseAnimationLoopType(split[8]) : AnimationLoopType.LoopForever;
|
||||
storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType);
|
||||
storyboard.GetLayer(layer).Add(storyboardSprite);
|
||||
break;
|
||||
@ -341,6 +339,12 @@ namespace osu.Game.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
private AnimationLoopType parseAnimationLoopType(string value)
|
||||
{
|
||||
var parsed = (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), value);
|
||||
return Enum.IsDefined(typeof(AnimationLoopType), parsed) ? parsed : AnimationLoopType.LoopForever;
|
||||
}
|
||||
|
||||
private void handleVariables(string line)
|
||||
{
|
||||
var pair = SplitKeyVal(line, '=');
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// The control points in this beatmap.
|
||||
/// </summary>
|
||||
ControlPointInfo ControlPointInfo { get; }
|
||||
ControlPointInfo ControlPointInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The breaks in this beatmap.
|
||||
@ -44,9 +44,13 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Returns statistics for the <see cref="HitObjects"/> contained in this beatmap.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<BeatmapStatistic> GetStatistics();
|
||||
|
||||
/// <summary>
|
||||
/// Finds the most common beat length represented by the control points in this beatmap.
|
||||
/// </summary>
|
||||
double GetMostCommonBeatLength();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a shallow-clone of this beatmap and returns it.
|
||||
/// </summary>
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Rulesets;
|
||||
@ -41,6 +42,11 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
ISkin Skin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="Track"/> which this <see cref="WorkingBeatmap"/> has loaded.
|
||||
/// </summary>
|
||||
Track Track { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a playable <see cref="IBeatmap"/> from <see cref="Beatmap"/> using the applicable converters for a specific <see cref="RulesetInfo"/>.
|
||||
/// <para>
|
||||
@ -67,5 +73,11 @@ namespace osu.Game.Beatmaps
|
||||
/// </remarks>
|
||||
/// <returns>A fresh track instance, which will also be available via <see cref="Track"/>.</returns>
|
||||
Track LoadTrack();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the stream of the file from the given storage path.
|
||||
/// </summary>
|
||||
/// <param name="storagePath">The storage path to the file.</param>
|
||||
Stream GetStream(string storagePath);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
// 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.ComponentModel;
|
||||
|
||||
namespace osu.Game.Beatmaps.Timing
|
||||
{
|
||||
public enum TimeSignatures
|
||||
{
|
||||
[Description("4/4")]
|
||||
SimpleQuadruple = 4,
|
||||
|
||||
[Description("3/4")]
|
||||
SimpleTriple = 3
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -12,7 +13,6 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
protected AudioManager AudioManager { get; }
|
||||
|
||||
private static readonly GlobalStatistic<int> total_count = GlobalStatistics.Get<int>(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s");
|
||||
|
||||
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
|
||||
{
|
||||
AudioManager = audioManager;
|
||||
@ -47,8 +45,6 @@ namespace osu.Game.Beatmaps
|
||||
waveform = new RecyclableLazy<Waveform>(GetWaveform);
|
||||
storyboard = new RecyclableLazy<Storyboard>(GetStoryboard);
|
||||
skin = new RecyclableLazy<ISkin>(GetSkin);
|
||||
|
||||
total_count.Value++;
|
||||
}
|
||||
|
||||
protected virtual Track GetVirtualTrack(double emptyLength = 0)
|
||||
@ -266,6 +262,26 @@ namespace osu.Game.Beatmaps
|
||||
[NotNull]
|
||||
public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
|
||||
/// </summary>
|
||||
public void PrepareTrackForPreviewLooping()
|
||||
{
|
||||
Track.Looping = true;
|
||||
Track.RestartPoint = Metadata.PreviewTime;
|
||||
|
||||
if (Track.RestartPoint == -1)
|
||||
{
|
||||
if (!Track.IsLoaded)
|
||||
{
|
||||
// force length to be populated (https://github.com/ppy/osu-framework/issues/4202)
|
||||
Track.Seek(Track.CurrentTime);
|
||||
}
|
||||
|
||||
Track.RestartPoint = 0.4f * Track.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfer a valid audio track into this working beatmap. Used as an optimisation to avoid reload / track swap
|
||||
/// across difficulties in the same beatmap set.
|
||||
@ -311,10 +327,7 @@ namespace osu.Game.Beatmaps
|
||||
protected virtual ISkin GetSkin() => new DefaultSkin();
|
||||
private readonly RecyclableLazy<ISkin> skin;
|
||||
|
||||
~WorkingBeatmap()
|
||||
{
|
||||
total_count.Value--;
|
||||
}
|
||||
public abstract Stream GetStream(string storagePath);
|
||||
|
||||
public class RecyclableLazy<T>
|
||||
{
|
||||
|
Reference in New Issue
Block a user