// Copyright (c) 2007-2017 ppy Pty Ltd . // 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; using System.Diagnostics; namespace osu.Game.Beatmaps { public abstract class WorkingBeatmap : IDisposable { public readonly BeatmapInfo BeatmapInfo; public readonly BeatmapSetInfo BeatmapSetInfo; public readonly BeatmapMetadata Metadata; public readonly Bindable> Mods = new Bindable>(new Mod[] { }); protected WorkingBeatmap(BeatmapInfo beatmapInfo) { BeatmapInfo = beatmapInfo; BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); Mods.ValueChanged += mods => applyRateAdjustments(); beatmap = new AsyncLazy(populateBeatmap); background = new AsyncLazy(populateBackground, b => b == null || !b.IsDisposed); track = new AsyncLazy(populateTrack); waveform = new AsyncLazy(populateWaveform); storyboard = new AsyncLazy(populateStoryboard); } /// /// Saves the . /// public void Save() { var path = FileSafety.GetTempPath(Guid.NewGuid().ToString().Replace("-", string.Empty) + ".json"); using (var sw = new StreamWriter(path)) sw.WriteLine(Beatmap.Serialize()); Process.Start(path); } protected abstract Beatmap GetBeatmap(); protected abstract Texture GetBackground(); protected abstract Track GetTrack(); protected virtual Waveform GetWaveform() => new Waveform(); protected virtual Storyboard GetStoryboard() => new Storyboard(); public bool BeatmapLoaded => beatmap.IsResultAvailable; public Beatmap Beatmap => beatmap.Value.Result; public async Task GetBeatmapAsync() => await beatmap.Value; private readonly AsyncLazy beatmap; private Beatmap populateBeatmap() { var b = GetBeatmap() ?? new Beatmap(); // use the database-backed info. b.BeatmapInfo = BeatmapInfo; return b; } public bool BackgroundLoaded => background.IsResultAvailable; public Texture Background => background.Value.Result; public async Task GetBackgroundAsync() => await background.Value; private AsyncLazy background; private Texture populateBackground() => GetBackground(); public bool TrackLoaded => track.IsResultAvailable; public Track Track => track.Value.Result; public async Task GetTrackAsync() => await track.Value; private AsyncLazy 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 GetWaveformAsync() => await waveform.Value; private readonly AsyncLazy waveform; private Waveform populateWaveform() => GetWaveform(); public bool StoryboardLoaded => storyboard.IsResultAvailable; public Storyboard Storyboard => storyboard.Value.Result; public async Task GetStoryboardAsync() => await storyboard.Value; private readonly AsyncLazy storyboard; private Storyboard populateStoryboard() => GetStoryboard(); 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(); } /// /// Eagerly dispose of the audio track associated with this (if any). /// Accessing track again will load a fresh instance. /// 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()) mod.ApplyToClock(t); } public class AsyncLazy { private Lazy> lazy; private readonly Func valueFactory; private readonly Func stillValidFunction; private readonly object initLock = new object(); public AsyncLazy(Func valueFactory, Func 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 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.Run(valueFactory)); } } }