1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 01:27:29 +08:00

Split out WorkingBeatmapCache from BeatmapManager

This commit is contained in:
Dean Herbert 2021-09-30 15:40:41 +09:00
parent 8a6501fa58
commit e7e0473323
3 changed files with 289 additions and 271 deletions

View File

@ -9,18 +9,13 @@ using System.Linq.Expressions;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Lists;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Database; using osu.Game.Database;
@ -31,7 +26,6 @@ using osu.Game.Online.API.Requests;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Users;
using Decoder = osu.Game.Beatmaps.Formats.Decoder; using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
@ -40,7 +34,7 @@ namespace osu.Game.Beatmaps
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
/// </summary> /// </summary>
[ExcludeFromDynamicCompile] [ExcludeFromDynamicCompile]
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IBeatmapResourceProvider public class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
{ {
/// <summary> /// <summary>
/// Fired when a single difficulty has been hidden. /// Fired when a single difficulty has been hidden.
@ -60,12 +54,12 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public Func<BeatmapSetInfo, CancellationToken, Task> PopulateOnlineInformation; public Func<BeatmapSetInfo, CancellationToken, Task> PopulateOnlineInformation;
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapRestored = new Bindable<WeakReference<BeatmapInfo>>();
/// <summary> /// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available. /// The game working beatmap cache, used to invalidate entries on changes.
/// </summary> /// </summary>
public readonly WorkingBeatmap DefaultBeatmap; public WorkingBeatmapCache WorkingBeatmapCache { private get; set; }
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapRestored = new Bindable<WeakReference<BeatmapInfo>>();
public override IEnumerable<string> HandledExtensions => new[] { ".osz" }; public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
@ -75,35 +69,19 @@ namespace osu.Game.Beatmaps
protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage();
private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps; private readonly BeatmapStore beatmaps;
private readonly AudioManager audioManager; private readonly RulesetStore rulesets;
private readonly IResourceStore<byte[]> resources;
private readonly LargeTextureStore largeTextureStore;
private readonly ITrackStore trackStore;
[CanBeNull] public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null)
private readonly GameHost host;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null,
WorkingBeatmap defaultBeatmap = null)
: base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host)
{ {
this.rulesets = rulesets; this.rulesets = rulesets;
this.audioManager = audioManager;
this.resources = resources;
this.host = host;
DefaultBeatmap = defaultBeatmap;
beatmaps = (BeatmapStore)ModelStore; beatmaps = (BeatmapStore)ModelStore;
beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference<BeatmapInfo>(b); beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference<BeatmapInfo>(b);
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b); beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b);
beatmaps.ItemRemoved += removeWorkingCache; beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b);
beatmaps.ItemUpdated += removeWorkingCache; beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj);
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store));
trackStore = audioManager.GetTrackStore(Files.Store);
} }
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
@ -111,33 +89,6 @@ namespace osu.Game.Beatmaps
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user)
{
var metadata = new BeatmapMetadata
{
Author = user,
};
var set = new BeatmapSetInfo
{
Metadata = metadata,
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
Ruleset = ruleset,
Metadata = metadata,
WidescreenStoryboard = true,
SamplesMatchPlaybackRate = true,
}
}
};
var working = Import(set).Result;
return GetWorkingBeatmap(working.Beatmaps.First());
}
protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
{ {
if (archive != null) if (archive != null)
@ -278,43 +229,7 @@ namespace osu.Game.Beatmaps
} }
} }
removeWorkingCache(info); WorkingBeatmapCache?.Invalidate(info);
}
private readonly WeakList<BeatmapManagerWorkingBeatmap> workingCache = new WeakList<BeatmapManagerWorkingBeatmap>();
/// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
/// </summary>
/// <param name="beatmapInfo">The beatmap to lookup.</param>
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
{
// if there are no files, presume the full beatmap info has not yet been fetched from the database.
if (beatmapInfo?.BeatmapSet?.Files.Count == 0)
{
int lookupId = beatmapInfo.ID;
beatmapInfo = QueryBeatmap(b => b.ID == lookupId);
}
if (beatmapInfo?.BeatmapSet == null)
return DefaultBeatmap;
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
if (working != null)
return working;
beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata;
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
// best effort; may be higher than expected.
GlobalStatistics.Get<int>(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count();
return working;
}
} }
/// <summary> /// <summary>
@ -515,35 +430,6 @@ namespace osu.Game.Beatmaps
return endTime - startTime; return endTime - startTime;
} }
private void removeWorkingCache(BeatmapSetInfo info)
{
if (info.Beatmaps == null) return;
foreach (var b in info.Beatmaps)
removeWorkingCache(b);
}
private void removeWorkingCache(BeatmapInfo info)
{
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID);
if (working != null)
workingCache.Remove(working);
}
}
#region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
AudioManager IStorageResourceProvider.AudioManager => audioManager;
IResourceStore<byte[]> IStorageResourceProvider.Files => Files.Store;
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
#endregion
/// <summary> /// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary> /// </summary>

View File

@ -1,147 +0,0 @@
// 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.
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Storyboards;
namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
[ExcludeFromDynamicCompile]
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{
[NotNull]
private readonly IBeatmapResourceProvider resources;
public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources)
: base(beatmapInfo, resources.AudioManager)
{
this.resources = resources;
}
protected override IBeatmap GetBeatmap()
{
if (BeatmapInfo.Path == null)
return new Beatmap { BeatmapInfo = BeatmapInfo };
try
{
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
}
catch (Exception e)
{
Logger.Error(e, "Beatmap failed to load");
return null;
}
}
protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes.
protected override Texture GetBackground()
{
if (Metadata?.BackgroundFile == null)
return null;
try
{
return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile));
}
catch (Exception e)
{
Logger.Error(e, "Background failed to load");
return null;
}
}
protected override Track GetBeatmapTrack()
{
if (Metadata?.AudioFile == null)
return null;
try
{
return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
}
catch (Exception e)
{
Logger.Error(e, "Track failed to load");
return null;
}
}
protected override Waveform GetWaveform()
{
if (Metadata?.AudioFile == null)
return null;
try
{
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
return trackData == null ? null : new Waveform(trackData);
}
catch (Exception e)
{
Logger.Error(e, "Waveform failed to load");
return null;
}
}
protected override Storyboard GetStoryboard()
{
Storyboard storyboard;
try
{
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
{
var decoder = Decoder.GetDecoder<Storyboard>(stream);
// todo: support loading from both set-wide storyboard *and* beatmap specific.
if (BeatmapSetInfo?.StoryboardFile == null)
storyboard = decoder.Decode(stream);
else
{
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile))))
storyboard = decoder.Decode(stream, secondaryStream);
}
}
}
catch (Exception e)
{
Logger.Error(e, "Storyboard failed to load");
storyboard = new Storyboard();
}
storyboard.BeatmapInfo = BeatmapInfo;
return storyboard;
}
protected internal override ISkin GetSkin()
{
try
{
return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources);
}
catch (Exception e)
{
Logger.Error(e, "Skin failed to load");
return null;
}
}
public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath);
}
}
}

View File

@ -0,0 +1,279 @@
// 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.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Users;
namespace osu.Game.Beatmaps
{
public class WorkingBeatmapCache : IBeatmapResourceProvider
{
private readonly WeakList<BeatmapManagerWorkingBeatmap> workingCache = new WeakList<BeatmapManagerWorkingBeatmap>();
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
public readonly WorkingBeatmap DefaultBeatmap;
public BeatmapManager BeatmapManager { private get; set; }
private readonly AudioManager audioManager;
private readonly IResourceStore<byte[]> resources;
private readonly LargeTextureStore largeTextureStore;
private readonly ITrackStore trackStore;
private readonly IResourceStore<byte[]> files;
[CanBeNull]
private readonly GameHost host;
public WorkingBeatmapCache([NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null, GameHost host = null)
{
DefaultBeatmap = defaultBeatmap;
this.audioManager = audioManager;
this.resources = resources;
this.host = host;
this.files = files;
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files));
trackStore = audioManager.GetTrackStore(files);
}
public void Invalidate(BeatmapSetInfo info)
{
if (info.Beatmaps == null) return;
foreach (var b in info.Beatmaps)
Invalidate(b);
}
public void Invalidate(BeatmapInfo info)
{
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID);
if (working != null)
workingCache.Remove(working);
}
}
/// <summary>
/// Create a new <see cref="WorkingBeatmap"/>.
/// </summary>
public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user)
{
var metadata = new BeatmapMetadata
{
Author = user,
};
var set = new BeatmapSetInfo
{
Metadata = metadata,
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
Ruleset = ruleset,
Metadata = metadata,
WidescreenStoryboard = true,
SamplesMatchPlaybackRate = true,
}
}
};
var working = BeatmapManager.Import(set).Result;
return GetWorkingBeatmap(working.Beatmaps.First());
}
/// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
/// </summary>
/// <param name="beatmapInfo">The beatmap to lookup.</param>
/// <returns>A <see cref="WorkingBeatmap"/> instance correlating to the provided <see cref="BeatmapInfo"/>.</returns>
public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
{
// if there are no files, presume the full beatmap info has not yet been fetched from the database.
if (beatmapInfo?.BeatmapSet?.Files.Count == 0)
{
int lookupId = beatmapInfo.ID;
beatmapInfo = BeatmapManager.QueryBeatmap(b => b.ID == lookupId);
}
if (beatmapInfo?.BeatmapSet == null)
return DefaultBeatmap;
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
if (working != null)
return working;
beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata;
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
// best effort; may be higher than expected.
GlobalStatistics.Get<int>(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count();
return working;
}
}
#region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
AudioManager IStorageResourceProvider.AudioManager => audioManager;
IResourceStore<byte[]> IStorageResourceProvider.Files => files;
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
#endregion
[ExcludeFromDynamicCompile]
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{
[NotNull]
private readonly IBeatmapResourceProvider resources;
public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources)
: base(beatmapInfo, resources.AudioManager)
{
this.resources = resources;
}
protected override IBeatmap GetBeatmap()
{
if (BeatmapInfo.Path == null)
return new Beatmap { BeatmapInfo = BeatmapInfo };
try
{
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
}
catch (Exception e)
{
Logger.Error(e, "Beatmap failed to load");
return null;
}
}
protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes.
protected override Texture GetBackground()
{
if (Metadata?.BackgroundFile == null)
return null;
try
{
return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile));
}
catch (Exception e)
{
Logger.Error(e, "Background failed to load");
return null;
}
}
protected override Track GetBeatmapTrack()
{
if (Metadata?.AudioFile == null)
return null;
try
{
return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
}
catch (Exception e)
{
Logger.Error(e, "Track failed to load");
return null;
}
}
protected override Waveform GetWaveform()
{
if (Metadata?.AudioFile == null)
return null;
try
{
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
return trackData == null ? null : new Waveform(trackData);
}
catch (Exception e)
{
Logger.Error(e, "Waveform failed to load");
return null;
}
}
protected override Storyboard GetStoryboard()
{
Storyboard storyboard;
try
{
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
{
var decoder = Decoder.GetDecoder<Storyboard>(stream);
// todo: support loading from both set-wide storyboard *and* beatmap specific.
if (BeatmapSetInfo?.StoryboardFile == null)
storyboard = decoder.Decode(stream);
else
{
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile))))
storyboard = decoder.Decode(stream, secondaryStream);
}
}
}
catch (Exception e)
{
Logger.Error(e, "Storyboard failed to load");
storyboard = new Storyboard();
}
storyboard.BeatmapInfo = BeatmapInfo;
return storyboard;
}
protected internal override ISkin GetSkin()
{
try
{
return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources);
}
catch (Exception e)
{
Logger.Error(e, "Skin failed to load");
return null;
}
}
public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath);
}
}
}