2019-01-24 08:43:03 +00:00
// 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 09:19:50 +00:00
using System ;
using System.Collections.Generic ;
2020-05-11 03:50:47 +00:00
using System.Diagnostics ;
2019-02-21 10:04:31 +00:00
using System.Linq ;
2018-09-06 03:51:23 +00:00
using System.Threading ;
2019-06-24 03:42:21 +00:00
using System.Threading.Tasks ;
2020-08-11 15:48:45 +00:00
using JetBrains.Annotations ;
2019-05-28 14:54:42 +00:00
using osu.Framework.Audio ;
2020-03-08 05:32:03 +00:00
using osu.Framework.Audio.Track ;
using osu.Framework.Graphics.Textures ;
using osu.Framework.Logging ;
2019-07-02 13:39:42 +00:00
using osu.Framework.Statistics ;
2020-09-04 11:34:26 +00:00
using osu.Framework.Testing ;
2018-04-19 13:04:12 +00:00
using osu.Game.Rulesets ;
2020-03-08 05:32:03 +00:00
using osu.Game.Rulesets.Mods ;
2019-05-29 07:43:27 +00:00
using osu.Game.Rulesets.Objects.Types ;
2018-04-19 13:04:12 +00:00
using osu.Game.Rulesets.UI ;
2018-04-13 09:19:50 +00:00
using osu.Game.Skinning ;
2020-03-08 05:32:03 +00:00
using osu.Game.Storyboards ;
2018-04-13 09:19:50 +00:00
namespace osu.Game.Beatmaps
{
2020-09-04 11:34:26 +00:00
[ExcludeFromDynamicCompile]
2020-02-10 08:01:41 +00:00
public abstract class WorkingBeatmap : IWorkingBeatmap
2018-04-13 09:19:50 +00:00
{
public readonly BeatmapInfo BeatmapInfo ;
public readonly BeatmapSetInfo BeatmapSetInfo ;
public readonly BeatmapMetadata Metadata ;
2019-05-31 05:40:53 +00:00
protected AudioManager AudioManager { get ; }
2019-07-02 13:39:42 +00:00
private static readonly GlobalStatistic < int > total_count = GlobalStatistics . Get < int > ( nameof ( Beatmaps ) , $"Total {nameof(WorkingBeatmap)}s" ) ;
2019-05-31 05:40:53 +00:00
protected WorkingBeatmap ( BeatmapInfo beatmapInfo , AudioManager audioManager )
2018-04-13 09:19:50 +00:00
{
2019-05-31 05:40:53 +00:00
AudioManager = audioManager ;
2018-04-13 09:19:50 +00:00
BeatmapInfo = beatmapInfo ;
BeatmapSetInfo = beatmapInfo . BeatmapSet ;
Metadata = beatmapInfo . Metadata ? ? BeatmapSetInfo ? . Metadata ? ? new BeatmapMetadata ( ) ;
2018-09-06 03:51:23 +00:00
background = new RecyclableLazy < Texture > ( GetBackground , BackgroundStillValid ) ;
waveform = new RecyclableLazy < Waveform > ( GetWaveform ) ;
storyboard = new RecyclableLazy < Storyboard > ( GetStoryboard ) ;
2019-08-28 10:57:17 +00:00
skin = new RecyclableLazy < ISkin > ( GetSkin ) ;
2019-07-02 13:39:42 +00:00
total_count . Value + + ;
2018-04-13 09:19:50 +00:00
}
2020-02-09 12:34:56 +00:00
protected virtual Track GetVirtualTrack ( double emptyLength = 0 )
2019-05-29 07:43:27 +00:00
{
const double excess_length = 1000 ;
2020-09-03 10:20:42 +00:00
var lastObject = Beatmap ? . HitObjects . LastOrDefault ( ) ;
2019-05-29 07:43:27 +00:00
double length ;
switch ( lastObject )
{
case null :
2020-02-09 12:34:56 +00:00
length = emptyLength ;
2019-05-29 07:43:27 +00:00
break ;
2020-05-27 03:38:39 +00:00
case IHasDuration endTime :
2019-05-29 07:43:27 +00:00
length = endTime . EndTime + excess_length ;
break ;
default :
length = lastObject . StartTime + excess_length ;
break ;
}
return AudioManager . Tracks . GetVirtual ( length ) ;
}
2019-07-31 10:48:50 +00:00
/// <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 04:52:40 +00:00
public IBeatmap GetPlayableBeatmap ( RulesetInfo ruleset , IReadOnlyList < Mod > mods = null , TimeSpan ? timeout = null )
2018-04-19 13:04:12 +00:00
{
2020-05-11 03:50:47 +00:00
using ( var cancellationSource = createCancellationTokenSource ( timeout ) )
2020-03-13 04:52:40 +00:00
{
mods ? ? = Array . Empty < Mod > ( ) ;
2019-12-12 06:58:11 +00:00
2020-03-13 04:52:40 +00:00
var rulesetInstance = ruleset . CreateInstance ( ) ;
2018-04-19 13:04:12 +00:00
2020-03-13 04:52:40 +00:00
IBeatmapConverter converter = CreateBeatmapConverter ( Beatmap , rulesetInstance ) ;
2018-04-19 13:04:12 +00:00
2020-03-13 04:52:40 +00: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})." ) ;
2018-04-19 13:04:12 +00:00
2020-03-13 04:52:40 +00:00
// Apply conversion mods
foreach ( var mod in mods . OfType < IApplicableToBeatmapConverter > ( ) )
{
2020-03-16 02:33:26 +00:00
if ( cancellationSource . IsCancellationRequested )
throw new BeatmapLoadTimeoutException ( BeatmapInfo ) ;
2018-04-19 13:04:12 +00:00
2020-03-13 04:52:40 +00:00
mod . ApplyToBeatmapConverter ( converter ) ;
}
2018-04-19 13:04:12 +00:00
2020-03-13 04:52:40 +00:00
// Convert
2020-09-17 08:40:05 +00:00
IBeatmap converted = converter . Convert ( cancellationSource . Token ) ;
2018-05-18 09:11:52 +00:00
2020-08-17 16:40:55 +00: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 04:52:40 +00:00
// Apply difficulty mods
if ( mods . Any ( m = > m is IApplicableToDifficulty ) )
{
converted . BeatmapInfo = converted . BeatmapInfo . Clone ( ) ;
converted . BeatmapInfo . BaseDifficulty = converted . BeatmapInfo . BaseDifficulty . Clone ( ) ;
2018-04-19 13:04:12 +00:00
2020-03-13 04:52:40 +00:00
foreach ( var mod in mods . OfType < IApplicableToDifficulty > ( ) )
{
2020-03-16 02:33:26 +00:00
if ( cancellationSource . IsCancellationRequested )
throw new BeatmapLoadTimeoutException ( BeatmapInfo ) ;
2018-06-29 03:45:48 +00:00
2020-03-13 04:52:40 +00:00
mod . ApplyToDifficulty ( converted . BeatmapInfo . BaseDifficulty ) ;
}
}
2018-06-29 03:45:48 +00:00
2020-03-13 04:52:40 +00:00
IBeatmapProcessor processor = rulesetInstance . CreateBeatmapProcessor ( converted ) ;
2018-04-19 13:04:12 +00:00
2020-03-13 04:52:40 +00:00
processor ? . PreProcess ( ) ;
2018-04-19 13:04:12 +00:00
2020-03-13 04:52:40 +00:00
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
2020-05-15 09:17:39 +00:00
try
2020-03-13 04:52:40 +00:00
{
2020-05-15 09:17:39 +00:00
foreach ( var obj in converted . HitObjects )
{
if ( cancellationSource . IsCancellationRequested )
throw new BeatmapLoadTimeoutException ( BeatmapInfo ) ;
2018-04-19 13:04:12 +00:00
2020-05-15 09:17:39 +00:00
obj . ApplyDefaults ( converted . ControlPointInfo , converted . BeatmapInfo . BaseDifficulty , cancellationSource . Token ) ;
}
}
catch ( OperationCanceledException )
{
throw new BeatmapLoadTimeoutException ( BeatmapInfo ) ;
2020-03-13 04:52:40 +00:00
}
2018-05-25 07:21:51 +00:00
2020-03-13 04:52:40 +00:00
foreach ( var mod in mods . OfType < IApplicableToHitObject > ( ) )
{
foreach ( var obj in converted . HitObjects )
{
2020-03-16 02:33:26 +00:00
if ( cancellationSource . IsCancellationRequested )
throw new BeatmapLoadTimeoutException ( BeatmapInfo ) ;
2020-03-13 04:52:40 +00:00
mod . ApplyToHitObject ( obj ) ;
}
}
processor ? . PostProcess ( ) ;
foreach ( var mod in mods . OfType < IApplicableToBeatmap > ( ) )
{
cancellationSource . Token . ThrowIfCancellationRequested ( ) ;
mod . ApplyToBeatmap ( converted ) ;
}
2019-08-01 03:41:46 +00:00
2020-03-13 04:52:40 +00:00
return converted ;
2019-11-11 11:53:22 +00:00
}
2018-04-19 13:04:12 +00:00
}
2020-02-10 08:01:41 +00:00
private CancellationTokenSource loadCancellation = new CancellationTokenSource ( ) ;
2018-07-19 09:43:11 +00:00
2020-02-10 08:01:41 +00:00
/// <summary>
/// Beings loading the contents of this <see cref="WorkingBeatmap"/> asynchronously.
/// </summary>
public void BeginAsyncLoad ( )
{
loadBeatmapAsync ( ) ;
}
2019-06-24 08:10:50 +00:00
2020-02-10 08:01:41 +00: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 ;
}
2020-05-11 03:50:47 +00:00
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 08:01:41 +00:00
private Task < IBeatmap > loadBeatmapAsync ( ) = > beatmapLoadTask ? ? = Task . Factory . StartNew ( ( ) = >
2019-06-24 08:10:50 +00:00
{
2019-07-02 13:22:33 +00:00
// Todo: Handle cancellation during beatmap parsing
2019-06-24 08:10:50 +00:00
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 08:01:41 +00:00
} , loadCancellation . Token , TaskCreationOptions . LongRunning , TaskScheduler . Default ) ;
public override string ToString ( ) = > BeatmapInfo . ToString ( ) ;
2020-04-30 07:42:38 +00:00
public virtual bool BeatmapLoaded = > beatmapLoadTask ? . IsCompleted ? ? false ;
2019-06-24 08:10:50 +00:00
2019-07-02 13:25:51 +00:00
public IBeatmap Beatmap
{
get
{
try
{
2020-02-10 08:01:41 +00:00
return loadBeatmapAsync ( ) . Result ;
2019-07-02 13:25:51 +00:00
}
2020-02-10 07:41:10 +00:00
catch ( AggregateException ae )
2019-07-02 13:25:51 +00:00
{
2020-02-10 08:25:11 +00: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 07:41:10 +00:00
2020-02-10 08:25:11 +00:00
Logger . Error ( ae , "Beatmap failed to load" ) ;
return null ;
}
catch ( Exception e )
{
Logger . Error ( e , "Beatmap failed to load" ) ;
2019-07-02 13:25:51 +00:00
return null ;
}
}
}
2019-06-24 08:10:50 +00:00
2018-09-06 03:51:23 +00:00
protected abstract IBeatmap GetBeatmap ( ) ;
2019-06-24 08:10:50 +00:00
private Task < IBeatmap > beatmapLoadTask ;
2018-04-13 09:19:50 +00:00
2018-09-06 03:51:23 +00:00
public bool BackgroundLoaded = > background . IsResultAvailable ;
public Texture Background = > background . Value ;
2018-09-11 02:28:02 +00:00
protected virtual bool BackgroundStillValid ( Texture b ) = > b = = null | | b . Available ;
2018-09-06 03:51:23 +00:00
protected abstract Texture GetBackground ( ) ;
private readonly RecyclableLazy < Texture > background ;
2018-04-13 09:19:50 +00:00
2020-08-17 06:38:16 +00:00
private Track loadedTrack ;
2020-08-11 15:48:45 +00:00
[NotNull]
2020-08-17 06:38:16 +00:00
public Track LoadTrack ( ) = > loadedTrack = GetBeatmapTrack ( ) ? ? GetVirtualTrack ( 1000 ) ;
2020-08-18 04:01:35 +00: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 ) ) ;
2020-08-21 04:53:12 +00:00
/// <summary>
/// Whether this beatmap's track has been loaded via <see cref="LoadTrack"/>.
/// </summary>
2020-08-21 06:05:56 +00:00
public virtual bool TrackLoaded = > loadedTrack ! = null ;
2020-08-21 04:53:12 +00:00
2020-08-17 06:38:16 +00: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
{
2020-08-21 04:53:12 +00:00
if ( ! TrackLoaded )
2020-08-17 06:38:16 +00:00
throw new InvalidOperationException ( $"Cannot access {nameof(Track)} without first calling {nameof(LoadTrack)}." ) ;
return loadedTrack ;
}
}
2020-08-04 12:53:00 +00:00
2020-08-07 13:31:41 +00:00
protected abstract Track GetBeatmapTrack ( ) ;
2018-04-13 09:19:50 +00:00
public bool WaveformLoaded = > waveform . IsResultAvailable ;
2018-09-06 03:51:23 +00:00
public Waveform Waveform = > waveform . Value ;
2019-01-07 09:50:27 +00:00
protected virtual Waveform GetWaveform ( ) = > new Waveform ( null ) ;
2018-09-06 03:51:23 +00:00
private readonly RecyclableLazy < Waveform > waveform ;
2018-04-13 09:19:50 +00:00
public bool StoryboardLoaded = > storyboard . IsResultAvailable ;
2018-09-06 03:51:23 +00:00
public Storyboard Storyboard = > storyboard . Value ;
protected virtual Storyboard GetStoryboard ( ) = > new Storyboard { BeatmapInfo = BeatmapInfo } ;
private readonly RecyclableLazy < Storyboard > storyboard ;
2018-04-13 09:19:50 +00:00
public bool SkinLoaded = > skin . IsResultAvailable ;
2019-08-28 10:57:17 +00:00
public ISkin Skin = > skin . Value ;
2019-05-28 14:54:42 +00:00
2019-08-28 10:57:17 +00:00
protected virtual ISkin GetSkin ( ) = > new DefaultSkin ( ) ;
private readonly RecyclableLazy < ISkin > skin ;
2018-04-13 09:19:50 +00:00
2019-07-02 13:21:56 +00:00
~ WorkingBeatmap ( )
{
2020-02-10 08:01:41 +00:00
total_count . Value - - ;
2019-07-02 13:21:56 +00:00
}
2018-09-06 03:51:23 +00:00
public class RecyclableLazy < T >
2018-04-13 09:19:50 +00:00
{
2018-09-06 03:51:23 +00:00
private Lazy < T > lazy ;
2018-04-13 09:19:50 +00:00
private readonly Func < T > valueFactory ;
private readonly Func < T , bool > stillValidFunction ;
2018-09-06 04:27:53 +00:00
private readonly object fetchLock = new object ( ) ;
2018-04-13 09:19:50 +00:00
2018-09-06 03:51:23 +00:00
public RecyclableLazy ( Func < T > valueFactory , Func < T , bool > stillValidFunction = null )
2018-04-13 09:19:50 +00:00
{
this . valueFactory = valueFactory ;
this . stillValidFunction = stillValidFunction ;
recreate ( ) ;
}
public void Recycle ( )
{
if ( ! IsResultAvailable ) return ;
2018-09-06 03:51:23 +00:00
( lazy . Value as IDisposable ) ? . Dispose ( ) ;
2018-04-13 09:19:50 +00:00
recreate ( ) ;
}
2018-09-06 03:51:23 +00:00
public bool IsResultAvailable = > stillValid ;
2018-04-13 09:19:50 +00:00
2018-09-06 03:51:23 +00:00
public T Value
2018-04-13 09:19:50 +00:00
{
get
{
2018-09-06 04:27:53 +00:00
lock ( fetchLock )
{
if ( ! stillValid )
recreate ( ) ;
return lazy . Value ;
}
2018-04-13 09:19:50 +00:00
}
}
2018-09-06 03:51:23 +00:00
private bool stillValid = > lazy . IsValueCreated & & ( stillValidFunction ? . Invoke ( lazy . Value ) ? ? true ) ;
private void recreate ( ) = > lazy = new Lazy < T > ( valueFactory , LazyThreadSafetyMode . ExecutionAndPublication ) ;
2018-04-13 09:19:50 +00:00
}
2020-03-16 02:33:26 +00:00
private class BeatmapLoadTimeoutException : TimeoutException
{
public BeatmapLoadTimeoutException ( BeatmapInfo beatmapInfo )
: base ( $"Timed out while loading beatmap ({beatmapInfo})." )
{
}
}
2018-04-13 09:19:50 +00:00
}
}