1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 20:38:33 +08:00
osu-lazer/osu.Game/Beatmaps/WorkingBeatmap.cs

306 lines
11 KiB
C#
Raw Normal View History

// 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;
using System.Threading.Tasks;
using osu.Framework.Audio;
using osu.Framework.Statistics;
2018-04-13 17:19:50 +08:00
using osu.Game.IO.Serialization;
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;
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;
protected AudioManager AudioManager { get; }
private static readonly GlobalStatistic<int> total_count = GlobalStatistics.Get<int>(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s");
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
2018-04-13 17:19:50 +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);
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
}
2018-05-07 09:23:32 +08:00
/// <summary>
/// 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>
/// <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>
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods)
{
var rulesetInstance = ruleset.CreateInstance();
2018-05-07 09:29:38 +08:00
IBeatmapConverter converter = rulesetInstance.CreateBeatmapConverter(Beatmap);
// 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}).");
// Apply conversion mods
2019-04-08 17:32:05 +08:00
foreach (var mod in mods.OfType<IApplicableToBeatmapConverter>())
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))
{
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>())
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);
2019-04-08 17:32:05 +08:00
foreach (var mod in mods.OfType<IApplicableToHitObject>())
foreach (var obj in converted.HitObjects)
mod.ApplyToHitObject(obj);
processor?.PostProcess();
return converted;
}
2018-07-19 17:43:11 +08:00
public override string ToString() => BeatmapInfo.ToString();
public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
public Task<IBeatmap> LoadBeatmapAsync() => (beatmapLoadTask ?? (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();
2018-09-06 11:51:23 +08:00
protected abstract IBeatmap GetBeatmap();
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;
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;
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>
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);
}
private bool isDisposed;
2019-06-27 12:56:36 +08:00
protected virtual void Dispose(bool isDisposing)
{
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.
beatmapCancellation.Cancel();
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
}
}
}