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
2017-03-12 13:13:43 +00:00
using System ;
using System.Collections.Generic ;
2021-11-18 14:26:45 +00:00
using System.Diagnostics ;
2021-04-17 15:47:13 +00:00
using System.IO ;
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 ;
2021-12-22 10:14:18 +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 ;
2022-01-03 08:31:12 +00:00
using osu.Framework.Extensions ;
2020-03-08 05:32:03 +00:00
using osu.Framework.Graphics.Textures ;
using osu.Framework.Logging ;
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 ;
2018-04-19 13:04:12 +00:00
using osu.Game.Rulesets.UI ;
2018-03-14 11:45:04 +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
2016-10-28 05:14:45 +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
2016-10-28 05:14:45 +00:00
{
2016-11-05 09:16:15 +00:00
public readonly BeatmapInfo BeatmapInfo ;
public readonly BeatmapSetInfo BeatmapSetInfo ;
2021-12-22 08:35:18 +00:00
// TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly).
2021-11-22 08:12:26 +00:00
public BeatmapMetadata Metadata = > BeatmapInfo . Metadata ;
2018-04-13 09:19:50 +00:00
2021-12-22 09:25:09 +00:00
public Waveform Waveform = > waveform . Value ;
public Storyboard Storyboard = > storyboard . Value ;
2021-12-22 10:14:18 +00:00
public Texture Background = > GetBackground ( ) ; // Texture uses ref counting, so we want to return a new instance every usage.
2021-12-22 09:25:09 +00:00
2021-12-22 10:14:18 +00:00
public ISkin Skin = > skin . Value ;
2021-12-22 09:25:09 +00:00
2021-12-22 10:14:18 +00:00
private AudioManager audioManager { get ; }
2021-12-22 08:39:13 +00:00
2021-12-22 10:14:18 +00:00
private CancellationTokenSource loadCancellationSource = new CancellationTokenSource ( ) ;
2021-12-22 08:39:13 +00:00
private readonly object beatmapFetchLock = new object ( ) ;
2019-05-31 05:40:53 +00:00
2021-12-22 09:16:57 +00:00
private readonly Lazy < Waveform > waveform ;
private readonly Lazy < Storyboard > storyboard ;
2021-12-22 10:14:18 +00:00
private readonly Lazy < ISkin > skin ;
private Track track ; // track is not Lazy as we allow transferring and loading multiple times.
2021-12-22 09:16:57 +00:00
2021-12-22 10:14:18 +00:00
protected WorkingBeatmap ( BeatmapInfo beatmapInfo , AudioManager audioManager )
2017-02-24 04:43:21 +00:00
{
2021-12-22 08:39:13 +00:00
this . audioManager = audioManager ;
2017-02-24 04:43:21 +00:00
BeatmapInfo = beatmapInfo ;
2021-11-22 08:12:26 +00:00
BeatmapSetInfo = beatmapInfo . BeatmapSet ? ? new BeatmapSetInfo ( ) ;
2018-04-13 09:19:50 +00:00
2021-12-22 09:16:57 +00:00
waveform = new Lazy < Waveform > ( GetWaveform ) ;
storyboard = new Lazy < Storyboard > ( GetStoryboard ) ;
2021-12-22 10:14:18 +00:00
skin = new Lazy < ISkin > ( GetSkin ) ;
2017-02-24 04:43:21 +00:00
}
2018-04-13 09:19:50 +00:00
2021-12-22 09:01:09 +00:00
#region Resource getters
protected virtual Waveform GetWaveform ( ) = > new Waveform ( null ) ;
protected virtual Storyboard GetStoryboard ( ) = > new Storyboard { BeatmapInfo = BeatmapInfo } ;
2021-12-22 10:14:18 +00:00
protected abstract IBeatmap GetBeatmap ( ) ;
protected abstract Texture GetBackground ( ) ;
protected abstract Track GetBeatmapTrack ( ) ;
2021-12-22 09:01:09 +00:00
/// <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>
2021-12-22 10:14:18 +00:00
protected internal abstract ISkin GetSkin ( ) ;
2021-12-22 09:01:09 +00:00
#endregion
#region Async load control
public void BeginAsyncLoad ( ) = > loadBeatmapAsync ( ) ;
public void CancelAsyncLoad ( )
{
lock ( beatmapFetchLock )
{
loadCancellationSource ? . Cancel ( ) ;
2021-12-22 10:14:18 +00:00
loadCancellationSource = new CancellationTokenSource ( ) ;
2021-12-22 09:01:09 +00:00
if ( beatmapLoadTask ? . IsCompleted ! = true )
beatmapLoadTask = null ;
}
}
#endregion
#region Track
2021-12-22 09:23:11 +00:00
public virtual bool TrackLoaded = > track ! = null ;
2021-12-22 09:01:09 +00:00
2021-12-22 09:23:11 +00:00
public Track LoadTrack ( ) = > track = GetBeatmapTrack ( ) ? ? GetVirtualTrack ( 1000 ) ;
2021-12-22 09:01:09 +00:00
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>
2022-05-20 11:43:07 +00:00
/// Attempts to transfer the audio track to a target working beatmap, if valid for transferring.
/// Used as an optimisation to avoid reload / track swap across difficulties in the same beatmap set.
2021-12-22 09:01:09 +00:00
/// </summary>
2022-05-20 11:43:07 +00:00
/// <param name="target">The target working beatmap to transfer this track to.</param>
2022-05-21 13:43:31 +00:00
/// <returns>Whether the track has been transferred to the <paramref name="target"/>.</returns>
2022-05-20 11:43:07 +00:00
public virtual bool TryTransferTrack ( [ NotNull ] WorkingBeatmap target )
{
2022-05-21 13:50:40 +00:00
if ( BeatmapInfo ? . AudioEquals ( target . BeatmapInfo ) ! = true | | track . IsDummyDevice )
2022-05-20 11:43:07 +00:00
return false ;
target . track = track ;
return true ;
}
2021-12-22 09:01:09 +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
{
if ( ! TrackLoaded )
throw new InvalidOperationException ( $"Cannot access {nameof(Track)} without first calling {nameof(LoadTrack)}." ) ;
2021-12-22 09:23:11 +00:00
return track ;
2021-12-22 09:01:09 +00:00
}
}
2021-12-22 08:39:13 +00:00
protected Track GetVirtualTrack ( double emptyLength = 0 )
2019-05-29 07:43:27 +00:00
{
const double excess_length = 1000 ;
2022-04-06 05:44:00 +00:00
double length = ( BeatmapInfo ? . Length + excess_length ) ? ? emptyLength ;
2019-05-29 07:43:27 +00:00
2021-12-22 10:14:18 +00:00
return audioManager . Tracks . GetVirtual ( length ) ;
2019-05-29 07:43:27 +00:00
}
2021-12-22 09:01:09 +00:00
#endregion
#region Beatmap
2021-12-22 09:23:11 +00:00
public virtual bool BeatmapLoaded = > beatmapLoadTask ? . IsCompleted ? ? false ;
2021-12-22 10:14:18 +00:00
public IBeatmap Beatmap
2021-12-22 09:01:09 +00:00
{
get
{
try
{
2022-01-06 13:54:43 +00:00
return loadBeatmapAsync ( ) . GetResultSafely ( ) ;
2021-12-22 09:01:09 +00:00
}
catch ( AggregateException ae )
{
// 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 ;
Logger . Error ( ae , "Beatmap failed to load" ) ;
return null ;
}
catch ( Exception e )
{
Logger . Error ( e , "Beatmap failed to load" ) ;
return null ;
}
}
}
2021-12-22 10:14:18 +00:00
private Task < IBeatmap > beatmapLoadTask ;
2021-12-22 09:01:09 +00:00
private Task < IBeatmap > loadBeatmapAsync ( )
{
lock ( beatmapFetchLock )
{
return 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 ;
2021-12-22 10:14:18 +00:00
} , loadCancellationSource . Token , TaskCreationOptions . LongRunning , TaskScheduler . Default ) ;
2021-12-22 09:01:09 +00:00
}
}
#endregion
#region Playable beatmap
2019-07-31 10:48:50 +00:00
2021-12-22 10:14:18 +00:00
public IBeatmap GetPlayableBeatmap ( IRulesetInfo ruleset , IReadOnlyList < Mod > mods = null )
2018-04-19 13:04:12 +00:00
{
2021-11-20 16:23:55 +00:00
try
{
using ( var cancellationTokenSource = new CancellationTokenSource ( 10_000 ) )
{
// don't apply the default timeout when debugger is attached (may be breakpointing / debugging).
2021-11-21 10:30:45 +00:00
return GetPlayableBeatmap ( ruleset , mods ? ? Array . Empty < Mod > ( ) , Debugger . IsAttached ? new CancellationToken ( ) : cancellationTokenSource . Token ) ;
2021-11-20 16:23:55 +00:00
}
}
catch ( OperationCanceledException )
{
throw new BeatmapLoadTimeoutException ( BeatmapInfo ) ;
}
}
2019-12-12 06:58:11 +00:00
2021-12-22 09:01:09 +00:00
public virtual IBeatmap GetPlayableBeatmap ( IRulesetInfo ruleset , IReadOnlyList < Mod > mods , CancellationToken token )
2021-11-20 16:23:55 +00:00
{
2021-11-08 05:33:32 +00:00
var rulesetInstance = ruleset . CreateInstance ( ) ;
2018-04-19 13:04:12 +00:00
2021-11-16 05:43:13 +00:00
if ( rulesetInstance = = null )
throw new RulesetLoadException ( "Creating ruleset instance failed when attempting to create playable beatmap." ) ;
2018-04-19 13:04:12 +00:00
2021-11-17 21:00:09 +00:00
IBeatmapConverter converter = CreateBeatmapConverter ( Beatmap , rulesetInstance ) ;
2021-11-08 05:33:32 +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
2021-11-08 05:33:32 +00:00
// Apply conversion mods
foreach ( var mod in mods . OfType < IApplicableToBeatmapConverter > ( ) )
{
2021-11-20 16:23:55 +00:00
token . ThrowIfCancellationRequested ( ) ;
2021-11-08 05:33:32 +00:00
mod . ApplyToBeatmapConverter ( converter ) ;
}
2018-04-19 13:04:12 +00:00
2021-11-08 05:33:32 +00:00
// Convert
2021-11-17 01:48:33 +00:00
IBeatmap converted = converter . Convert ( token ) ;
2018-05-18 09:11:52 +00:00
2021-11-08 05:33:32 +00:00
// Apply conversion mods to the result
foreach ( var mod in mods . OfType < IApplicableAfterBeatmapConversion > ( ) )
{
2021-11-20 16:23:55 +00:00
token . ThrowIfCancellationRequested ( ) ;
2021-11-08 05:33:32 +00:00
mod . ApplyToBeatmap ( converted ) ;
}
2020-08-17 16:40:55 +00:00
2021-11-08 05:33:32 +00:00
// Apply difficulty mods
if ( mods . Any ( m = > m is IApplicableToDifficulty ) )
{
foreach ( var mod in mods . OfType < IApplicableToDifficulty > ( ) )
2020-03-13 04:52:40 +00:00
{
2021-11-20 16:23:55 +00:00
token . ThrowIfCancellationRequested ( ) ;
2021-11-08 05:33:32 +00:00
mod . ApplyToDifficulty ( converted . Difficulty ) ;
2020-03-13 04:52:40 +00:00
}
2021-11-08 05:33:32 +00:00
}
2018-06-29 03:45:48 +00:00
2021-11-08 05:33:32 +00:00
IBeatmapProcessor processor = rulesetInstance . CreateBeatmapProcessor ( converted ) ;
2018-04-19 13:04:12 +00:00
2021-11-08 05:33:32 +00:00
foreach ( var mod in mods . OfType < IApplicableToBeatmapProcessor > ( ) )
mod . ApplyToBeatmapProcessor ( processor ) ;
2021-06-23 05:08:24 +00:00
2021-11-08 05:33:32 +00:00
processor ? . PreProcess ( ) ;
2018-04-19 13:04:12 +00:00
2021-11-08 05:33:32 +00:00
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
2021-11-20 16:23:55 +00:00
foreach ( var obj in converted . HitObjects )
2021-11-08 05:33:32 +00:00
{
2021-11-20 16:23:55 +00:00
token . ThrowIfCancellationRequested ( ) ;
obj . ApplyDefaults ( converted . ControlPointInfo , converted . Difficulty , token ) ;
2021-11-08 05:33:32 +00:00
}
2018-05-25 07:21:51 +00:00
2021-11-08 05:33:32 +00:00
foreach ( var mod in mods . OfType < IApplicableToHitObject > ( ) )
{
foreach ( var obj in converted . HitObjects )
2020-03-13 04:52:40 +00:00
{
2021-11-20 16:23:55 +00:00
token . ThrowIfCancellationRequested ( ) ;
2021-11-08 05:33:32 +00:00
mod . ApplyToHitObject ( obj ) ;
2020-03-13 04:52:40 +00:00
}
2021-11-08 05:33:32 +00:00
}
2020-03-13 04:52:40 +00:00
2021-11-08 05:33:32 +00:00
processor ? . PostProcess ( ) ;
2019-08-01 03:41:46 +00:00
2021-11-08 05:33:32 +00:00
foreach ( var mod in mods . OfType < IApplicableToBeatmap > ( ) )
{
2021-11-17 01:48:33 +00:00
token . ThrowIfCancellationRequested ( ) ;
2021-11-08 05:33:32 +00:00
mod . ApplyToBeatmap ( converted ) ;
2019-11-11 11:53:22 +00:00
}
2021-11-08 05:33:32 +00:00
return converted ;
2018-04-19 13:04:12 +00:00
}
2020-08-17 06:38:16 +00:00
/// <summary>
2021-12-22 09:01:09 +00:00
/// Creates a <see cref="IBeatmapConverter"/> to convert a <see cref="IBeatmap"/> for a specified <see cref="Ruleset"/>.
2020-08-17 06:38:16 +00:00
/// </summary>
2021-12-22 09:01:09 +00:00
/// <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 ) ;
2018-04-13 09:19:50 +00:00
2021-12-22 09:01:09 +00:00
#endregion
2019-05-28 14:54:42 +00:00
2021-12-22 09:01:09 +00:00
public override string ToString ( ) = > BeatmapInfo . ToString ( ) ;
2021-05-30 06:19:47 +00:00
2021-12-22 10:14:18 +00:00
public abstract Stream GetStream ( string storagePath ) ;
2021-04-17 15:47:13 +00:00
2021-12-22 09:16:57 +00:00
IBeatmapInfo IWorkingBeatmap . BeatmapInfo = > BeatmapInfo ;
2020-03-16 02:33:26 +00:00
private class BeatmapLoadTimeoutException : TimeoutException
{
public BeatmapLoadTimeoutException ( BeatmapInfo beatmapInfo )
: base ( $"Timed out while loading beatmap ({beatmapInfo})." )
{
}
}
2016-10-28 05:14:45 +00:00
}
}