1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 03:42:58 +08:00
osu-lazer/osu.Game/Beatmaps/WorkingBeatmap.cs

397 lines
15 KiB
C#
Raw Normal View History

// 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.
2018-04-13 17:19:50 +08:00
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
2019-02-21 18:04:31 +08:00
using System.Linq;
2018-09-06 11:51:23 +08:00
using System.Threading;
using System.Threading.Tasks;
2020-08-11 23:48:45 +08:00
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
2019-05-29 15:43:27 +08:00
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI;
2018-04-13 17:19:50 +08:00
using osu.Game.Skinning;
using osu.Game.Storyboards;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
2020-02-10 16:01:41 +08:00
public abstract class WorkingBeatmap : IWorkingBeatmap
2018-04-13 17:19:50 +08:00
{
public readonly BeatmapInfo BeatmapInfo;
public readonly BeatmapSetInfo BeatmapSetInfo;
public readonly BeatmapMetadata Metadata;
protected AudioManager AudioManager { get; }
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
2018-04-13 17:19:50 +08:00
{
AudioManager = audioManager;
2018-04-13 17:19:50 +08:00
BeatmapInfo = beatmapInfo;
BeatmapSetInfo = beatmapInfo.BeatmapSet;
Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
2018-09-06 11:51:23 +08:00
background = new RecyclableLazy<Texture>(GetBackground, BackgroundStillValid);
waveform = new RecyclableLazy<Waveform>(GetWaveform);
storyboard = new RecyclableLazy<Storyboard>(GetStoryboard);
skin = new RecyclableLazy<ISkin>(GetSkin);
2018-04-13 17:19:50 +08:00
}
protected virtual Track GetVirtualTrack(double emptyLength = 0)
2019-05-29 15:43:27 +08:00
{
const double excess_length = 1000;
var lastObject = Beatmap?.HitObjects.LastOrDefault();
2019-05-29 15:43:27 +08:00
double length;
switch (lastObject)
{
case null:
length = emptyLength;
2019-05-29 15:43:27 +08:00
break;
2020-05-27 11:38:39 +08:00
case IHasDuration endTime:
2019-05-29 15:43:27 +08:00
length = endTime.EndTime + excess_length;
break;
default:
length = lastObject.StartTime + excess_length;
break;
}
return AudioManager.Tracks.GetVirtual(length);
}
/// <summary>
/// Creates a <see cref="IBeatmapConverter"/> to convert a <see cref="IBeatmap"/> for a specified <see cref="Ruleset"/>.
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> to be converted.</param>
/// <param name="ruleset">The <see cref="Ruleset"/> for which <paramref name="beatmap"/> should be converted.</param>
/// <returns>The applicable <see cref="IBeatmapConverter"/>.</returns>
protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap);
2020-03-13 12:52:40 +08:00
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
{
using (var cancellationSource = createCancellationTokenSource(timeout))
2020-03-13 12:52:40 +08:00
{
mods ??= Array.Empty<Mod>();
2020-03-13 12:52:40 +08:00
var rulesetInstance = ruleset.CreateInstance();
2020-03-13 12:52:40 +08:00
IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance);
2020-03-13 12:52:40 +08:00
// Check if the beatmap can be converted
if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert())
throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter}).");
2020-03-13 12:52:40 +08:00
// Apply conversion mods
foreach (var mod in mods.OfType<IApplicableToBeatmapConverter>())
{
2020-03-16 10:33:26 +08:00
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
2020-03-13 12:52:40 +08:00
mod.ApplyToBeatmapConverter(converter);
}
2020-03-13 12:52:40 +08:00
// Convert
IBeatmap converted = converter.Convert(cancellationSource.Token);
2020-08-18 00:40:55 +08:00
// Apply conversion mods to the result
foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>())
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToBeatmap(converted);
}
2020-03-13 12:52:40 +08:00
// Apply difficulty mods
if (mods.Any(m => m is IApplicableToDifficulty))
{
converted.BeatmapInfo = converted.BeatmapInfo.Clone();
converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone();
2020-03-13 12:52:40 +08:00
foreach (var mod in mods.OfType<IApplicableToDifficulty>())
{
2020-03-16 10:33:26 +08:00
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
2020-03-13 12:52:40 +08:00
mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty);
}
}
2020-03-13 12:52:40 +08:00
IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted);
foreach (var mod in mods.OfType<IApplicableToBeatmapProcessor>())
mod.ApplyToBeatmapProcessor(processor);
2020-03-13 12:52:40 +08:00
processor?.PreProcess();
2020-03-13 12:52:40 +08:00
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
2020-05-15 17:17:39 +08:00
try
2020-03-13 12:52:40 +08:00
{
2020-05-15 17:17:39 +08:00
foreach (var obj in converted.HitObjects)
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
2020-05-15 17:17:39 +08:00
obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty, cancellationSource.Token);
}
}
catch (OperationCanceledException)
{
throw new BeatmapLoadTimeoutException(BeatmapInfo);
2020-03-13 12:52:40 +08:00
}
2020-03-13 12:52:40 +08:00
foreach (var mod in mods.OfType<IApplicableToHitObject>())
{
foreach (var obj in converted.HitObjects)
{
2020-03-16 10:33:26 +08:00
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
2020-03-13 12:52:40 +08:00
mod.ApplyToHitObject(obj);
}
}
processor?.PostProcess();
foreach (var mod in mods.OfType<IApplicableToBeatmap>())
{
cancellationSource.Token.ThrowIfCancellationRequested();
mod.ApplyToBeatmap(converted);
}
2020-03-13 12:52:40 +08:00
return converted;
2019-11-11 19:53:22 +08:00
}
}
2020-02-10 16:01:41 +08:00
private CancellationTokenSource loadCancellation = new CancellationTokenSource();
2018-07-19 17:43:11 +08:00
2020-02-10 16:01:41 +08:00
/// <summary>
/// Beings loading the contents of this <see cref="WorkingBeatmap"/> asynchronously.
/// </summary>
public void BeginAsyncLoad()
{
loadBeatmapAsync();
}
2020-02-10 16:01:41 +08:00
/// <summary>
/// Cancels the asynchronous loading of the contents of this <see cref="WorkingBeatmap"/>.
/// </summary>
public void CancelAsyncLoad()
{
loadCancellation?.Cancel();
loadCancellation = new CancellationTokenSource();
if (beatmapLoadTask?.IsCompleted != true)
beatmapLoadTask = null;
}
private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout)
{
if (Debugger.IsAttached)
// ignore timeout when debugger is attached (may be breakpointing / debugging).
return new CancellationTokenSource();
return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10));
}
2020-02-10 16:01:41 +08:00
private Task<IBeatmap> loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
{
// Todo: Handle cancellation during beatmap parsing
var b = GetBeatmap() ?? new Beatmap();
// The original beatmap version needs to be preserved as the database doesn't contain it
BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
// Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
b.BeatmapInfo = BeatmapInfo;
return b;
2020-02-10 16:01:41 +08:00
}, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
public override string ToString() => BeatmapInfo.ToString();
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
public IBeatmap Beatmap
{
get
{
try
{
2020-02-10 16:01:41 +08:00
return loadBeatmapAsync().Result;
}
catch (AggregateException ae)
{
2020-02-10 16:25:11 +08:00
// This is the exception that is generally expected here, which occurs via natural cancellation of the asynchronous load
if (ae.InnerExceptions.FirstOrDefault() is TaskCanceledException)
return null;
2020-02-10 16:25:11 +08:00
Logger.Error(ae, "Beatmap failed to load");
return null;
}
catch (Exception e)
{
Logger.Error(e, "Beatmap failed to load");
return null;
}
}
}
2018-09-06 11:51:23 +08:00
protected abstract IBeatmap GetBeatmap();
private Task<IBeatmap> beatmapLoadTask;
2018-04-13 17:19:50 +08:00
2018-09-06 11:51:23 +08:00
public bool BackgroundLoaded => background.IsResultAvailable;
public Texture Background => background.Value;
protected virtual bool BackgroundStillValid(Texture b) => b == null || b.Available;
2018-09-06 11:51:23 +08:00
protected abstract Texture GetBackground();
private readonly RecyclableLazy<Texture> background;
2018-04-13 17:19:50 +08:00
2020-08-17 14:38:16 +08:00
private Track loadedTrack;
2020-08-11 23:48:45 +08:00
[NotNull]
2020-08-17 14:38:16 +08:00
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;
}
}
2020-08-18 12:01:35 +08:00
/// <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.
/// </summary>
/// <param name="track">The track to transfer.</param>
public void TransferTrack([NotNull] Track track) => loadedTrack = track ?? throw new ArgumentNullException(nameof(track));
/// <summary>
/// Whether this beatmap's track has been loaded via <see cref="LoadTrack"/>.
/// </summary>
public virtual bool TrackLoaded => loadedTrack != null;
2020-08-17 14:38:16 +08:00
/// <summary>
/// Get the loaded audio track instance. <see cref="LoadTrack"/> must have first been called.
/// This generally happens via MusicController when changing the global beatmap.
/// </summary>
public Track Track
{
get
{
if (!TrackLoaded)
2020-08-17 14:38:16 +08:00
throw new InvalidOperationException($"Cannot access {nameof(Track)} without first calling {nameof(LoadTrack)}.");
return loadedTrack;
}
}
2020-08-07 21:31:41 +08:00
protected abstract Track GetBeatmapTrack();
2018-04-13 17:19:50 +08:00
public bool WaveformLoaded => waveform.IsResultAvailable;
2018-09-06 11:51:23 +08:00
public Waveform Waveform => waveform.Value;
2019-01-07 17:50:27 +08:00
protected virtual Waveform GetWaveform() => new Waveform(null);
2018-09-06 11:51:23 +08:00
private readonly RecyclableLazy<Waveform> waveform;
2018-04-13 17:19:50 +08:00
public bool StoryboardLoaded => storyboard.IsResultAvailable;
2018-09-06 11:51:23 +08:00
public Storyboard Storyboard => storyboard.Value;
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
private readonly RecyclableLazy<Storyboard> storyboard;
2018-04-13 17:19:50 +08:00
public bool SkinLoaded => skin.IsResultAvailable;
public ISkin Skin => skin.Value;
/// <summary>
/// Creates a new skin instance for this beatmap.
/// </summary>
/// <remarks>
/// This should only be called externally in scenarios where it is explicitly desired to get a new instance of a skin
/// (e.g. for editing purposes, to avoid state pollution).
/// For standard reading purposes, <see cref="Skin"/> should always be used directly.
/// </remarks>
protected internal abstract ISkin GetSkin();
private readonly RecyclableLazy<ISkin> skin;
2018-04-13 17:19:50 +08:00
public abstract Stream GetStream(string storagePath);
2018-09-06 11:51:23 +08:00
public class RecyclableLazy<T>
2018-04-13 17:19:50 +08:00
{
2018-09-06 11:51:23 +08:00
private Lazy<T> lazy;
2018-04-13 17:19:50 +08:00
private readonly Func<T> valueFactory;
private readonly Func<T, bool> stillValidFunction;
2018-09-06 12:27:53 +08:00
private readonly object fetchLock = new object();
2018-04-13 17:19:50 +08:00
2018-09-06 11:51:23 +08:00
public RecyclableLazy(Func<T> valueFactory, Func<T, bool> stillValidFunction = null)
2018-04-13 17:19:50 +08:00
{
this.valueFactory = valueFactory;
this.stillValidFunction = stillValidFunction;
recreate();
}
public void Recycle()
{
if (!IsResultAvailable) return;
2018-09-06 11:51:23 +08:00
(lazy.Value as IDisposable)?.Dispose();
2018-04-13 17:19:50 +08:00
recreate();
}
2018-09-06 11:51:23 +08:00
public bool IsResultAvailable => stillValid;
2018-04-13 17:19:50 +08:00
2018-09-06 11:51:23 +08:00
public T Value
2018-04-13 17:19:50 +08:00
{
get
{
2018-09-06 12:27:53 +08:00
lock (fetchLock)
{
if (!stillValid)
recreate();
return lazy.Value;
}
2018-04-13 17:19:50 +08:00
}
}
2018-09-06 11:51:23 +08:00
private bool stillValid => lazy.IsValueCreated && (stillValidFunction?.Invoke(lazy.Value) ?? true);
private void recreate() => lazy = new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
2018-04-13 17:19:50 +08:00
}
2020-03-16 10:33:26 +08:00
private class BeatmapLoadTimeoutException : TimeoutException
{
public BeatmapLoadTimeoutException(BeatmapInfo beatmapInfo)
: base($"Timed out while loading beatmap ({beatmapInfo}).")
{
}
}
2018-04-13 17:19:50 +08:00
}
}