2019-01-24 16:43:03 +08: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 17:19:50 +08:00
using osu.Framework.Audio.Track ;
using osu.Framework.Graphics.Textures ;
using osu.Game.Rulesets.Mods ;
using System ;
using System.Collections.Generic ;
using osu.Game.Storyboards ;
using osu.Framework.IO.File ;
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 ;
2019-06-24 11:42:21 +08:00
using System.Threading.Tasks ;
2019-05-28 22:54:42 +08:00
using osu.Framework.Audio ;
2019-07-02 21:39:42 +08:00
using osu.Framework.Statistics ;
2018-04-13 17:19:50 +08:00
using osu.Game.IO.Serialization ;
2018-04-19 21:04:12 +08:00
using osu.Game.Rulesets ;
2018-07-21 10:38:28 +08:00
using osu.Game.Rulesets.Objects ;
2019-05-29 15:43:27 +08:00
using osu.Game.Rulesets.Objects.Types ;
2018-04-19 21:04:12 +08:00
using osu.Game.Rulesets.UI ;
2018-04-13 17:19:50 +08:00
using osu.Game.Skinning ;
namespace osu.Game.Beatmaps
{
2019-05-29 15:43:27 +08:00
public abstract class WorkingBeatmap : IDisposable
2018-04-13 17:19:50 +08:00
{
public readonly BeatmapInfo BeatmapInfo ;
public readonly BeatmapSetInfo BeatmapSetInfo ;
public readonly BeatmapMetadata Metadata ;
2019-05-31 13:40:53 +08:00
protected AudioManager AudioManager { get ; }
2019-07-02 21:39:42 +08:00
private static readonly GlobalStatistic < int > total_count = GlobalStatistics . Get < int > ( nameof ( Beatmaps ) , $"Total {nameof(WorkingBeatmap)}s" ) ;
2019-05-31 13:40:53 +08:00
protected WorkingBeatmap ( BeatmapInfo beatmapInfo , AudioManager audioManager )
2018-04-13 17:19:50 +08:00
{
2019-05-31 13:40:53 +08:00
AudioManager = audioManager ;
2018-04-13 17:19:50 +08:00
BeatmapInfo = beatmapInfo ;
BeatmapSetInfo = beatmapInfo . BeatmapSet ;
Metadata = beatmapInfo . Metadata ? ? BeatmapSetInfo ? . Metadata ? ? new BeatmapMetadata ( ) ;
2019-06-04 10:25:18 +08:00
track = new RecyclableLazy < Track > ( ( ) = > GetTrack ( ) ? ? GetVirtualTrack ( ) ) ;
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 < Skin > ( GetSkin ) ;
2019-07-02 21:39:42 +08:00
total_count . Value + + ;
2018-04-13 17:19:50 +08:00
}
2019-06-04 10:25:18 +08:00
protected virtual Track GetVirtualTrack ( )
2019-05-29 15:43:27 +08:00
{
const double excess_length = 1000 ;
2019-06-04 10:25:18 +08:00
var lastObject = Beatmap . HitObjects . LastOrDefault ( ) ;
2019-05-29 15:43:27 +08:00
double length ;
switch ( lastObject )
{
case null :
length = excess_length ;
break ;
case IHasEndTime endTime :
length = endTime . EndTime + excess_length ;
break ;
default :
length = lastObject . StartTime + excess_length ;
break ;
}
return AudioManager . Tracks . GetVirtual ( length ) ;
}
2018-04-13 17:19:50 +08:00
/// <summary>
2018-05-07 09:29:38 +08:00
/// Saves the <see cref="Beatmaps.Beatmap"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-06-19 19:19:52 +08:00
/// <returns>The absolute path of the output file.</returns>
public string Save ( )
2018-04-13 17:19:50 +08:00
{
var path = FileSafety . GetTempPath ( Guid . NewGuid ( ) . ToString ( ) . Replace ( "-" , string . Empty ) + ".json" ) ;
using ( var sw = new StreamWriter ( path ) )
2018-05-07 09:29:38 +08:00
sw . WriteLine ( Beatmap . Serialize ( ) ) ;
2018-06-19 19:19:52 +08:00
return path ;
2018-04-13 17:19:50 +08:00
}
2019-07-31 18:48:50 +08: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 ) ;
2018-05-07 09:23:32 +08:00
/// <summary>
2018-05-07 09:40:30 +08:00
/// Constructs a playable <see cref="IBeatmap"/> from <see cref="Beatmap"/> using the applicable converters for a specific <see cref="RulesetInfo"/>.
/// <para>
/// The returned <see cref="IBeatmap"/> is in a playable state - all <see cref="HitObject"/> and <see cref="BeatmapDifficulty"/> <see cref="Mod"/>s
/// have been applied, and <see cref="HitObject"/>s have been fully constructed.
/// </para>
2018-05-07 09:23:32 +08:00
/// </summary>
2018-05-07 09:40:30 +08:00
/// <param name="ruleset">The <see cref="RulesetInfo"/> to create a playable <see cref="IBeatmap"/> for.</param>
2019-04-25 16:36:17 +08:00
/// <param name="mods">The <see cref="Mod"/>s to apply to the <see cref="IBeatmap"/>.</param>
2018-05-07 09:23:32 +08:00
/// <returns>The converted <see cref="IBeatmap"/>.</returns>
2018-05-07 09:29:38 +08:00
/// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception>
2019-04-10 16:13:12 +08:00
public IBeatmap GetPlayableBeatmap ( RulesetInfo ruleset , IReadOnlyList < Mod > mods )
2018-04-19 21:04:12 +08:00
{
var rulesetInstance = ruleset . CreateInstance ( ) ;
2019-07-31 18:48:50 +08:00
IBeatmapConverter converter = CreateBeatmapConverter ( Beatmap , rulesetInstance ) ;
2018-04-19 21:04:12 +08:00
// Check if the beatmap can be converted
if ( ! converter . CanConvert )
2018-05-07 13:28:30 +08:00
throw new BeatmapInvalidForRulesetException ( $"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter})." ) ;
2018-04-19 21:04:12 +08:00
// Apply conversion mods
2019-04-08 17:32:05 +08:00
foreach ( var mod in mods . OfType < IApplicableToBeatmapConverter > ( ) )
2018-04-19 21:04:12 +08:00
mod . ApplyToBeatmapConverter ( converter ) ;
// Convert
IBeatmap converted = converter . Convert ( ) ;
// Apply difficulty mods
2019-04-08 17:32:05 +08:00
if ( mods . Any ( m = > m is IApplicableToDifficulty ) )
2018-05-18 17:11:52 +08:00
{
converted . BeatmapInfo = converted . BeatmapInfo . Clone ( ) ;
converted . BeatmapInfo . BaseDifficulty = converted . BeatmapInfo . BaseDifficulty . Clone ( ) ;
2019-04-08 17:32:05 +08:00
foreach ( var mod in mods . OfType < IApplicableToDifficulty > ( ) )
2018-05-18 17:11:52 +08:00
mod . ApplyToDifficulty ( converted . BeatmapInfo . BaseDifficulty ) ;
}
2018-04-19 21:04:12 +08:00
2018-06-29 11:45:48 +08:00
IBeatmapProcessor processor = rulesetInstance . CreateBeatmapProcessor ( converted ) ;
processor ? . PreProcess ( ) ;
2018-04-19 21:04:12 +08:00
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
foreach ( var obj in converted . HitObjects )
obj . ApplyDefaults ( converted . ControlPointInfo , converted . BeatmapInfo . BaseDifficulty ) ;
2019-04-08 17:32:05 +08:00
foreach ( var mod in mods . OfType < IApplicableToHitObject > ( ) )
2018-04-19 21:04:12 +08:00
foreach ( var obj in converted . HitObjects )
mod . ApplyToHitObject ( obj ) ;
2018-06-29 11:45:48 +08:00
processor ? . PostProcess ( ) ;
2018-05-25 15:21:51 +08:00
2019-08-01 11:41:46 +08:00
foreach ( var mod in mods . OfType < IApplicableToBeatmap > ( ) )
2019-08-01 12:37:40 +08:00
mod . ApplyToBeatmap ( converted ) ;
2019-08-01 11:41:46 +08:00
2018-04-19 21:04:12 +08:00
return converted ;
}
2018-07-19 17:43:11 +08:00
public override string ToString ( ) = > BeatmapInfo . ToString ( ) ;
2019-06-24 16:10:50 +08:00
public bool BeatmapLoaded = > beatmapLoadTask ? . IsCompleted ? ? false ;
public Task < IBeatmap > LoadBeatmapAsync ( ) = > ( beatmapLoadTask ? ? ( beatmapLoadTask = Task . Factory . StartNew ( ( ) = >
{
2019-07-02 21:22:33 +08:00
// Todo: Handle cancellation during beatmap parsing
2019-06-24 16:10:50 +08: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 ;
} , beatmapCancellation . Token , TaskCreationOptions . LongRunning , TaskScheduler . Default ) ) ) ;
2019-07-02 21:25:51 +08:00
public IBeatmap Beatmap
{
get
{
try
{
return LoadBeatmapAsync ( ) . Result ;
}
catch ( TaskCanceledException )
{
return null ;
}
}
}
2019-06-24 16:10:50 +08:00
2019-06-24 11:42:21 +08:00
private readonly CancellationTokenSource beatmapCancellation = new CancellationTokenSource ( ) ;
2018-09-06 11:51:23 +08:00
protected abstract IBeatmap GetBeatmap ( ) ;
2019-06-24 16:10:50 +08:00
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 ;
2018-09-11 10:28:02 +08:00
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
public bool TrackLoaded = > track . IsResultAvailable ;
2018-09-06 11:51:23 +08:00
public Track Track = > track . Value ;
protected abstract Track GetTrack ( ) ;
private RecyclableLazy < Track > track ;
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 ;
2018-09-06 11:51:23 +08:00
public Skin Skin = > skin . Value ;
2019-05-28 22:54:42 +08:00
2018-09-06 11:51:23 +08:00
protected virtual Skin GetSkin ( ) = > new DefaultSkin ( ) ;
private readonly RecyclableLazy < Skin > skin ;
2018-04-13 17:19:50 +08:00
2018-09-06 11:51:23 +08:00
/// <summary>
/// Transfer pieces of a beatmap to a new one, where possible, to save on loading.
/// </summary>
/// <param name="other">The new beatmap which is being switched to.</param>
public virtual void TransferTo ( WorkingBeatmap other )
2018-04-13 17:19:50 +08:00
{
if ( track . IsResultAvailable & & Track ! = null & & BeatmapInfo . AudioEquals ( other . BeatmapInfo ) )
other . track = track ;
}
/// <summary>
/// Eagerly dispose of the audio track associated with this <see cref="WorkingBeatmap"/> (if any).
/// Accessing track again will load a fresh instance.
/// </summary>
2019-05-28 22:54:42 +08:00
public virtual void RecycleTrack ( ) = > track . Recycle ( ) ;
2018-04-13 17:19:50 +08:00
2019-06-27 12:56:36 +08:00
#region Disposal
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
2019-07-02 21:39:42 +08:00
private bool isDisposed ;
2019-06-27 12:56:36 +08:00
protected virtual void Dispose ( bool isDisposing )
{
2019-07-02 21:39:42 +08:00
if ( isDisposed )
return ;
isDisposed = true ;
2019-06-27 12:56:36 +08:00
// recycling logic is not here for the time being, as components which use
// retrieved objects from WorkingBeatmap may not hold a reference to the WorkingBeatmap itself.
2019-07-02 21:24:08 +08:00
// this should be fine as each retrieved component do have their own finalizers.
2019-06-27 12:56:36 +08:00
// cancelling the beatmap load is safe for now since the retrieval is a synchronous
// operation. if we add an async retrieval method this may need to be reconsidered.
2019-07-22 14:01:01 +08:00
beatmapCancellation ? . Cancel ( ) ;
2019-07-02 21:39:42 +08:00
total_count . Value - - ;
2019-06-27 12:56:36 +08:00
}
2019-07-02 21:21:56 +08:00
~ WorkingBeatmap ( )
{
Dispose ( false ) ;
}
2019-06-27 12:56:36 +08:00
#endregion
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
}
}
}