// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. 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 System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Audio; using osu.Framework.Statistics; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osu.Framework.Graphics.Video; namespace osu.Game.Beatmaps { public abstract class WorkingBeatmap : IWorkingBeatmap, IDisposable { public readonly BeatmapInfo BeatmapInfo; public readonly BeatmapSetInfo BeatmapSetInfo; public readonly BeatmapMetadata Metadata; protected AudioManager AudioManager { get; } private static readonly GlobalStatistic total_count = GlobalStatistics.Get(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s"); protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager) { AudioManager = audioManager; BeatmapInfo = beatmapInfo; BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); track = new RecyclableLazy(() => GetTrack() ?? GetVirtualTrack(1000)); background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); skin = new RecyclableLazy(GetSkin); total_count.Value++; } protected virtual Track GetVirtualTrack(double emptyLength = 0) { const double excess_length = 1000; var lastObject = Beatmap.HitObjects.LastOrDefault(); double length; switch (lastObject) { case null: length = emptyLength; break; case IHasEndTime endTime: length = endTime.EndTime + excess_length; break; default: length = lastObject.StartTime + excess_length; break; } return AudioManager.Tracks.GetVirtual(length); } /// /// Creates a to convert a for a specified . /// /// The to be converted. /// The for which should be converted. /// The applicable . protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap); public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null) { mods ??= Array.Empty(); var rulesetInstance = ruleset.CreateInstance(); IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); // 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})."); // Apply conversion mods foreach (var mod in mods.OfType()) mod.ApplyToBeatmapConverter(converter); // Convert IBeatmap converted = converter.Convert(); // Apply difficulty mods if (mods.Any(m => m is IApplicableToDifficulty)) { converted.BeatmapInfo = converted.BeatmapInfo.Clone(); converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); foreach (var mod in mods.OfType()) mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); } IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted); processor?.PreProcess(); // 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.OfType()) { foreach (var obj in converted.HitObjects) mod.ApplyToHitObject(obj); } processor?.PostProcess(); foreach (var mod in mods.OfType()) mod.ApplyToBeatmap(converted); return converted; } public override string ToString() => BeatmapInfo.ToString(); public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false; public Task 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; }, beatmapCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); public IBeatmap Beatmap { get { try { return LoadBeatmapAsync().Result; } catch (TaskCanceledException) { return null; } } } private readonly CancellationTokenSource beatmapCancellation = new CancellationTokenSource(); protected abstract IBeatmap GetBeatmap(); private Task beatmapLoadTask; public bool BackgroundLoaded => background.IsResultAvailable; public Texture Background => background.Value; protected virtual bool BackgroundStillValid(Texture b) => b == null || b.Available; protected abstract Texture GetBackground(); private readonly RecyclableLazy background; public VideoSprite Video => GetVideo(); protected abstract VideoSprite GetVideo(); public bool TrackLoaded => track.IsResultAvailable; public Track Track => track.Value; protected abstract Track GetTrack(); private RecyclableLazy track; public bool WaveformLoaded => waveform.IsResultAvailable; public Waveform Waveform => waveform.Value; protected virtual Waveform GetWaveform() => new Waveform(null); private readonly RecyclableLazy waveform; public bool StoryboardLoaded => storyboard.IsResultAvailable; public Storyboard Storyboard => storyboard.Value; protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; private readonly RecyclableLazy storyboard; public bool SkinLoaded => skin.IsResultAvailable; public ISkin Skin => skin.Value; protected virtual ISkin GetSkin() => new DefaultSkin(); private readonly RecyclableLazy skin; /// /// Transfer pieces of a beatmap to a new one, where possible, to save on loading. /// /// The new beatmap which is being switched to. public virtual void TransferTo(WorkingBeatmap other) { if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) other.track = track; } /// /// Eagerly dispose of the audio track associated with this (if any). /// Accessing track again will load a fresh instance. /// public virtual void RecycleTrack() => track.Recycle(); #region Disposal public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private bool isDisposed; protected virtual void Dispose(bool isDisposing) { if (isDisposed) return; isDisposed = true; // 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. // this should be fine as each retrieved component do have their own finalizers. // 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. beatmapCancellation?.Cancel(); total_count.Value--; } ~WorkingBeatmap() { Dispose(false); } #endregion public class RecyclableLazy { private Lazy lazy; private readonly Func valueFactory; private readonly Func stillValidFunction; private readonly object fetchLock = new object(); public RecyclableLazy(Func valueFactory, Func stillValidFunction = null) { this.valueFactory = valueFactory; this.stillValidFunction = stillValidFunction; recreate(); } public void Recycle() { if (!IsResultAvailable) return; (lazy.Value as IDisposable)?.Dispose(); recreate(); } public bool IsResultAvailable => stillValid; public T Value { get { lock (fetchLock) { if (!stillValid) recreate(); return lazy.Value; } } } private bool stillValid => lazy.IsValueCreated && (stillValidFunction?.Invoke(lazy.Value) ?? true); private void recreate() => lazy = new Lazy(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); } } }