2018-04-13 17:19:50 +08:00
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Audio.Track ;
using osu.Framework.Configuration ;
using osu.Framework.Graphics.Textures ;
using osu.Game.Rulesets.Mods ;
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading.Tasks ;
using osu.Game.Storyboards ;
using osu.Framework.IO.File ;
using System.IO ;
using osu.Game.IO.Serialization ;
2018-04-19 21:04:12 +08:00
using osu.Game.Rulesets ;
using osu.Game.Rulesets.UI ;
2018-04-13 17:19:50 +08:00
using osu.Game.Skinning ;
namespace osu.Game.Beatmaps
{
public abstract class WorkingBeatmap : IDisposable
{
public readonly BeatmapInfo BeatmapInfo ;
public readonly BeatmapSetInfo BeatmapSetInfo ;
public readonly BeatmapMetadata Metadata ;
public readonly Bindable < IEnumerable < Mod > > Mods = new Bindable < IEnumerable < Mod > > ( new Mod [ ] { } ) ;
protected WorkingBeatmap ( BeatmapInfo beatmapInfo )
{
BeatmapInfo = beatmapInfo ;
BeatmapSetInfo = beatmapInfo . BeatmapSet ;
Metadata = beatmapInfo . Metadata ? ? BeatmapSetInfo ? . Metadata ? ? new BeatmapMetadata ( ) ;
Mods . ValueChanged + = mods = > applyRateAdjustments ( ) ;
2018-05-07 09:29:38 +08:00
beatmap = new AsyncLazy < IBeatmap > ( populateBeatmap ) ;
2018-04-13 17:19:50 +08:00
background = new AsyncLazy < Texture > ( populateBackground , b = > b = = null | | ! b . IsDisposed ) ;
track = new AsyncLazy < Track > ( populateTrack ) ;
waveform = new AsyncLazy < Waveform > ( populateWaveform ) ;
storyboard = new AsyncLazy < Storyboard > ( populateStoryboard ) ;
skin = new AsyncLazy < Skin > ( populateSkin ) ;
}
/// <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
}
2018-05-07 10:22:25 +08:00
protected abstract IBeatmap GetBeatmap ( ) ;
2018-04-13 17:19:50 +08:00
protected abstract Texture GetBackground ( ) ;
protected abstract Track GetTrack ( ) ;
protected virtual Skin GetSkin ( ) = > new DefaultSkin ( ) ;
protected virtual Waveform GetWaveform ( ) = > new Waveform ( ) ;
protected virtual Storyboard GetStoryboard ( ) = > new Storyboard { BeatmapInfo = BeatmapInfo } ;
2018-05-07 09:29:38 +08:00
public bool BeatmapLoaded = > beatmap . IsResultAvailable ;
public IBeatmap Beatmap = > beatmap . Value . Result ;
public async Task < IBeatmap > GetBeatmapAsync ( ) = > await beatmap . Value ;
private readonly AsyncLazy < IBeatmap > beatmap ;
2018-04-13 17:19:50 +08:00
2018-05-07 09:29:38 +08:00
private IBeatmap populateBeatmap ( )
2018-04-13 17:19:50 +08:00
{
2018-05-07 10:22:25 +08:00
var b = GetBeatmap ( ) ? ? new Beatmap ( ) ;
2018-04-13 17:19:50 +08:00
// use the database-backed info.
b . BeatmapInfo = BeatmapInfo ;
return b ;
}
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>
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>
2018-05-07 09:40:30 +08:00
public IBeatmap GetPlayableBeatmap ( RulesetInfo ruleset )
2018-04-19 21:04:12 +08:00
{
var rulesetInstance = ruleset . CreateInstance ( ) ;
2018-05-07 09:29:38 +08:00
IBeatmapConverter converter = rulesetInstance . CreateBeatmapConverter ( Beatmap ) ;
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
foreach ( var mod in Mods . Value . OfType < IApplicableToBeatmapConverter > ( ) )
mod . ApplyToBeatmapConverter ( converter ) ;
// Convert
IBeatmap converted = converter . Convert ( ) ;
// Apply difficulty mods
2018-05-18 17:11:52 +08:00
if ( Mods . Value . Any ( m = > m is IApplicableToDifficulty ) )
{
converted . BeatmapInfo = converted . BeatmapInfo . Clone ( ) ;
converted . BeatmapInfo . BaseDifficulty = converted . BeatmapInfo . BaseDifficulty . Clone ( ) ;
foreach ( var mod in Mods . Value . OfType < IApplicableToDifficulty > ( ) )
mod . ApplyToDifficulty ( converted . BeatmapInfo . BaseDifficulty ) ;
}
2018-04-19 21:04:12 +08:00
// Post-process
rulesetInstance . CreateBeatmapProcessor ( converted ) ? . PostProcess ( ) ;
// 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 ) ;
foreach ( var mod in Mods . Value . OfType < IApplicableToHitObject > ( ) )
foreach ( var obj in converted . HitObjects )
mod . ApplyToHitObject ( obj ) ;
return converted ;
}
2018-04-13 17:19:50 +08:00
public bool BackgroundLoaded = > background . IsResultAvailable ;
public Texture Background = > background . Value . Result ;
public async Task < Texture > GetBackgroundAsync ( ) = > await background . Value ;
private AsyncLazy < Texture > background ;
private Texture populateBackground ( ) = > GetBackground ( ) ;
public bool TrackLoaded = > track . IsResultAvailable ;
public Track Track = > track . Value . Result ;
public async Task < Track > GetTrackAsync ( ) = > await track . Value ;
private AsyncLazy < Track > track ;
private Track populateTrack ( )
{
// we want to ensure that we always have a track, even if it's a fake one.
var t = GetTrack ( ) ? ? new TrackVirtual ( ) ;
applyRateAdjustments ( t ) ;
return t ;
}
public bool WaveformLoaded = > waveform . IsResultAvailable ;
public Waveform Waveform = > waveform . Value . Result ;
public async Task < Waveform > GetWaveformAsync ( ) = > await waveform . Value ;
private readonly AsyncLazy < Waveform > waveform ;
private Waveform populateWaveform ( ) = > GetWaveform ( ) ;
public bool StoryboardLoaded = > storyboard . IsResultAvailable ;
public Storyboard Storyboard = > storyboard . Value . Result ;
public async Task < Storyboard > GetStoryboardAsync ( ) = > await storyboard . Value ;
private readonly AsyncLazy < Storyboard > storyboard ;
private Storyboard populateStoryboard ( ) = > GetStoryboard ( ) ;
public bool SkinLoaded = > skin . IsResultAvailable ;
public Skin Skin = > skin . Value . Result ;
public async Task < Skin > GetSkinAsync ( ) = > await skin . Value ;
private readonly AsyncLazy < Skin > skin ;
private Skin populateSkin ( ) = > GetSkin ( ) ;
public void TransferTo ( WorkingBeatmap other )
{
if ( track . IsResultAvailable & & Track ! = null & & BeatmapInfo . AudioEquals ( other . BeatmapInfo ) )
other . track = track ;
if ( background . IsResultAvailable & & Background ! = null & & BeatmapInfo . BackgroundEquals ( other . BeatmapInfo ) )
other . background = background ;
}
public virtual void Dispose ( )
{
if ( BackgroundLoaded ) Background ? . Dispose ( ) ;
if ( WaveformLoaded ) Waveform ? . Dispose ( ) ;
if ( StoryboardLoaded ) Storyboard ? . Dispose ( ) ;
if ( SkinLoaded ) Skin ? . Dispose ( ) ;
}
/// <summary>
/// Eagerly dispose of the audio track associated with this <see cref="WorkingBeatmap"/> (if any).
/// Accessing track again will load a fresh instance.
/// </summary>
public void RecycleTrack ( ) = > track . Recycle ( ) ;
private void applyRateAdjustments ( Track t = null )
{
if ( t = = null & & track . IsResultAvailable ) t = Track ;
if ( t = = null ) return ;
t . ResetSpeedAdjustments ( ) ;
foreach ( var mod in Mods . Value . OfType < IApplicableToClock > ( ) )
mod . ApplyToClock ( t ) ;
}
public class AsyncLazy < T >
{
private Lazy < Task < T > > lazy ;
private readonly Func < T > valueFactory ;
private readonly Func < T , bool > stillValidFunction ;
private readonly object initLock = new object ( ) ;
public AsyncLazy ( Func < T > valueFactory , Func < T , bool > stillValidFunction = null )
{
this . valueFactory = valueFactory ;
this . stillValidFunction = stillValidFunction ;
recreate ( ) ;
}
public void Recycle ( )
{
if ( ! IsResultAvailable ) return ;
( lazy . Value . Result as IDisposable ) ? . Dispose ( ) ;
recreate ( ) ;
}
public bool IsResultAvailable
{
get
{
recreateIfInvalid ( ) ;
return lazy . Value . IsCompleted ;
}
}
public Task < T > Value
{
get
{
recreateIfInvalid ( ) ;
return lazy . Value ;
}
}
private void recreateIfInvalid ( )
{
lock ( initLock )
{
if ( ! lazy . IsValueCreated | | ! lazy . Value . IsCompleted )
// we have not yet been initialised or haven't run the task.
return ;
if ( stillValidFunction ? . Invoke ( lazy . Value . Result ) ? ? true )
// we are still in a valid state.
return ;
recreate ( ) ;
}
}
private void recreate ( ) = > lazy = new Lazy < Task < T > > ( ( ) = > Task . Run ( valueFactory ) ) ;
}
}
}