mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 12:17:26 +08:00
Merge pull request #14899 from peppy/beatmap-manager-split
Split `BeatmapManager` into two pieces
This commit is contained in:
commit
bee14a0c55
@ -158,18 +158,34 @@ namespace osu.Game.Tests.Online
|
||||
|
||||
public Task<BeatmapSetInfo> CurrentImportTask { get; private set; }
|
||||
|
||||
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize)
|
||||
=> new TestDownloadRequest(set);
|
||||
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host)
|
||||
{
|
||||
return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host);
|
||||
}
|
||||
|
||||
public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null)
|
||||
: base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
internal class TestBeatmapModelManager : BeatmapModelManager
|
||||
{
|
||||
await AllowImport.Task.ConfigureAwait(false);
|
||||
return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false);
|
||||
private readonly TestBeatmapManager testBeatmapManager;
|
||||
|
||||
public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost)
|
||||
: base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost)
|
||||
{
|
||||
this.testBeatmapManager = testBeatmapManager;
|
||||
}
|
||||
|
||||
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize)
|
||||
=> new TestDownloadRequest(set);
|
||||
|
||||
public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await testBeatmapManager.AllowImport.Task.ConfigureAwait(false);
|
||||
return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,110 +6,59 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
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.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Users;
|
||||
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
|
||||
/// Handles general operations related to global beatmap management.
|
||||
/// </summary>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IBeatmapResourceProvider
|
||||
public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been hidden.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapHidden => beatmapHidden;
|
||||
|
||||
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapHidden = new Bindable<WeakReference<BeatmapInfo>>();
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been restored.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapRestored;
|
||||
|
||||
/// <summary>
|
||||
/// An online lookup queue component which handles populating online beatmap metadata.
|
||||
/// </summary>
|
||||
public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; }
|
||||
|
||||
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapRestored = new Bindable<WeakReference<BeatmapInfo>>();
|
||||
|
||||
/// <summary>
|
||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||
/// </summary>
|
||||
public readonly WorkingBeatmap DefaultBeatmap;
|
||||
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
|
||||
protected override string ImportFromStablePath => ".";
|
||||
|
||||
protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage();
|
||||
|
||||
private readonly RulesetStore rulesets;
|
||||
private readonly BeatmapStore beatmaps;
|
||||
private readonly AudioManager audioManager;
|
||||
private readonly IResourceStore<byte[]> resources;
|
||||
private readonly LargeTextureStore largeTextureStore;
|
||||
private readonly ITrackStore trackStore;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly GameHost host;
|
||||
private readonly BeatmapModelManager beatmapModelManager;
|
||||
private readonly WorkingBeatmapCache workingBeatmapCache;
|
||||
private readonly BeatmapOnlineLookupQueue onlineBetamapLookupQueue;
|
||||
|
||||
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)
|
||||
WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
this.audioManager = audioManager;
|
||||
this.resources = resources;
|
||||
this.host = host;
|
||||
beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host);
|
||||
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host);
|
||||
|
||||
DefaultBeatmap = defaultBeatmap;
|
||||
workingBeatmapCache.BeatmapManager = beatmapModelManager;
|
||||
|
||||
beatmaps = (BeatmapStore)ModelStore;
|
||||
beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference<BeatmapInfo>(b);
|
||||
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b);
|
||||
beatmaps.ItemRemoved += removeWorkingCache;
|
||||
beatmaps.ItemUpdated += removeWorkingCache;
|
||||
|
||||
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store));
|
||||
trackStore = audioManager.GetTrackStore(Files.Store);
|
||||
if (performOnlineLookups)
|
||||
{
|
||||
onlineBetamapLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
|
||||
beatmapModelManager.OnlineLookupQueue = onlineBetamapLookupQueue;
|
||||
}
|
||||
}
|
||||
|
||||
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
|
||||
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
|
||||
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host) =>
|
||||
new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host);
|
||||
|
||||
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
|
||||
protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) =>
|
||||
new BeatmapModelManager(storage, contextFactory, rulesets, api, host);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="WorkingBeatmap"/>.
|
||||
/// </summary>
|
||||
public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user)
|
||||
{
|
||||
var metadata = new BeatmapMetadata
|
||||
@ -133,112 +82,21 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
};
|
||||
|
||||
var working = Import(set).Result;
|
||||
var working = beatmapModelManager.Import(set).Result;
|
||||
return GetWorkingBeatmap(working.Beatmaps.First());
|
||||
}
|
||||
|
||||
protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (archive != null)
|
||||
beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files);
|
||||
|
||||
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
|
||||
{
|
||||
// remove metadata from difficulties where it matches the set
|
||||
if (beatmapSet.Metadata.Equals(b.Metadata))
|
||||
b.Metadata = null;
|
||||
|
||||
b.BeatmapSet = beatmapSet;
|
||||
}
|
||||
|
||||
validateOnlineIds(beatmapSet);
|
||||
|
||||
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
|
||||
|
||||
if (OnlineLookupQueue != null)
|
||||
await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
||||
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
|
||||
{
|
||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||
{
|
||||
beatmapSet.OnlineBeatmapSetID = null;
|
||||
LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PreImport(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
|
||||
throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
|
||||
|
||||
// check if a set already exists with the same online id, delete if it does.
|
||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||
{
|
||||
var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
|
||||
|
||||
if (existingOnlineId != null)
|
||||
{
|
||||
Delete(existingOnlineId);
|
||||
|
||||
// in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
|
||||
existingOnlineId.OnlineBeatmapSetID = null;
|
||||
foreach (var b in existingOnlineId.Beatmaps)
|
||||
b.OnlineBeatmapID = null;
|
||||
|
||||
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOnlineIds(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
|
||||
|
||||
// ensure all IDs are unique
|
||||
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
|
||||
{
|
||||
LogForModel(beatmapSet, "Found non-unique IDs, resetting...");
|
||||
resetIds();
|
||||
return;
|
||||
}
|
||||
|
||||
// find any existing beatmaps in the database that have matching online ids
|
||||
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList();
|
||||
|
||||
if (existingBeatmaps.Count > 0)
|
||||
{
|
||||
// reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
|
||||
// we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
|
||||
var existing = CheckForExisting(beatmapSet);
|
||||
|
||||
if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b)))
|
||||
{
|
||||
LogForModel(beatmapSet, "Found existing import with IDs already, resetting...");
|
||||
resetIds();
|
||||
}
|
||||
}
|
||||
|
||||
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
|
||||
}
|
||||
|
||||
protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable<BeatmapSetInfo> items)
|
||||
=> base.CheckLocalAvailability(model, items)
|
||||
|| (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID));
|
||||
#region Delegation to BeatmapModelManager (methods which previously existed locally).
|
||||
|
||||
/// <summary>
|
||||
/// Delete a beatmap difficulty.
|
||||
/// Fired when a single difficulty has been hidden.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap difficulty to hide.</param>
|
||||
public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap);
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapHidden => beatmapModelManager.BeatmapHidden;
|
||||
|
||||
/// <summary>
|
||||
/// Restore a beatmap difficulty.
|
||||
/// Fired when a single difficulty has been restored.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap difficulty to restore.</param>
|
||||
public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap);
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapModelManager.BeatmapRestored;
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
||||
@ -246,109 +104,13 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="info">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
||||
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
||||
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
||||
public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null)
|
||||
{
|
||||
var setInfo = info.BeatmapSet;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID);
|
||||
var metadata = beatmapInfo.Metadata ?? setInfo.Metadata;
|
||||
|
||||
// grab the original file (or create a new one if not found).
|
||||
var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo();
|
||||
|
||||
// metadata may have changed; update the path with the standard format.
|
||||
beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu";
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
|
||||
// update existing or populate new file's filename.
|
||||
fileInfo.Filename = beatmapInfo.Path;
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
ReplaceFile(setInfo, fileInfo, stream);
|
||||
}
|
||||
}
|
||||
|
||||
removeWorkingCache(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>
|
||||
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
|
||||
|
||||
protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||
{
|
||||
if (!base.CanSkipImport(existing, import))
|
||||
return false;
|
||||
|
||||
return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null);
|
||||
}
|
||||
|
||||
protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||
{
|
||||
if (!base.CanReuseExisting(existing, import))
|
||||
return false;
|
||||
|
||||
var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
|
||||
var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
|
||||
|
||||
// force re-import if we are not in a sane state.
|
||||
return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds);
|
||||
}
|
||||
public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) => beatmapModelManager.Save(info, beatmapContent, beatmapSkin);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||
public List<BeatmapSetInfo> GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) =>
|
||||
GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList();
|
||||
public List<BeatmapSetInfo> GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includes, includeProtected);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. Note that files are not populated.
|
||||
@ -356,34 +118,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="includes">The level of detail to include in the returned objects.</param>
|
||||
/// <param name="includeProtected">Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases.</param>
|
||||
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||
public IEnumerable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false)
|
||||
{
|
||||
IQueryable<BeatmapSetInfo> queryable;
|
||||
|
||||
switch (includes)
|
||||
{
|
||||
case IncludedDetails.Minimal:
|
||||
queryable = beatmaps.BeatmapSetsOverview;
|
||||
break;
|
||||
|
||||
case IncludedDetails.AllButRuleset:
|
||||
queryable = beatmaps.BeatmapSetsWithoutRuleset;
|
||||
break;
|
||||
|
||||
case IncludedDetails.AllButFiles:
|
||||
queryable = beatmaps.BeatmapSetsWithoutFiles;
|
||||
break;
|
||||
|
||||
default:
|
||||
queryable = beatmaps.ConsumableItems;
|
||||
break;
|
||||
}
|
||||
|
||||
// AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY
|
||||
// clause which causes queries to take 5-10x longer.
|
||||
// TODO: remove if upgrading to EF core 3.x.
|
||||
return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected));
|
||||
}
|
||||
public IEnumerable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includes, includeProtected);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
||||
@ -391,202 +126,197 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="includes">The level of detail to include in the returned objects.</param>
|
||||
/// <returns>Results from the provided query.</returns>
|
||||
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query, IncludedDetails includes = IncludedDetails.All)
|
||||
{
|
||||
IQueryable<BeatmapSetInfo> queryable;
|
||||
|
||||
switch (includes)
|
||||
{
|
||||
case IncludedDetails.Minimal:
|
||||
queryable = beatmaps.BeatmapSetsOverview;
|
||||
break;
|
||||
|
||||
case IncludedDetails.AllButRuleset:
|
||||
queryable = beatmaps.BeatmapSetsWithoutRuleset;
|
||||
break;
|
||||
|
||||
case IncludedDetails.AllButFiles:
|
||||
queryable = beatmaps.BeatmapSetsWithoutFiles;
|
||||
break;
|
||||
|
||||
default:
|
||||
queryable = beatmaps.ConsumableItems;
|
||||
break;
|
||||
}
|
||||
|
||||
return queryable.AsNoTracking().Where(query);
|
||||
}
|
||||
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query, IncludedDetails includes = IncludedDetails.All) => beatmapModelManager.QueryBeatmapSets(query, includes);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapInfo QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query);
|
||||
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmapModelManager.QueryBeatmapSet(query);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>Results from the provided query.</returns>
|
||||
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
|
||||
|
||||
protected override string HumanisedModelName => "beatmap";
|
||||
|
||||
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
|
||||
{
|
||||
// let's make sure there are actually .osu files to import.
|
||||
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (string.IsNullOrEmpty(mapName))
|
||||
{
|
||||
Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database);
|
||||
return null;
|
||||
}
|
||||
|
||||
Beatmap beatmap;
|
||||
using (var stream = new LineBufferedReader(reader.GetStream(mapName)))
|
||||
beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
|
||||
|
||||
return new BeatmapSetInfo
|
||||
{
|
||||
OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
|
||||
Beatmaps = new List<BeatmapInfo>(),
|
||||
Metadata = beatmap.Metadata,
|
||||
DateAdded = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmapModelManager.QueryBeatmaps(query);
|
||||
|
||||
/// <summary>
|
||||
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
/// </summary>
|
||||
private List<BeatmapInfo> createBeatmapDifficulties(List<BeatmapSetFileInfo> files)
|
||||
{
|
||||
var beatmapInfos = new List<BeatmapInfo>();
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapInfo QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => beatmapModelManager.QueryBeatmap(query);
|
||||
|
||||
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
|
||||
using (var ms = new MemoryStream()) // we need a memory stream so we can seek
|
||||
using (var sr = new LineBufferedReader(ms))
|
||||
{
|
||||
raw.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
/// <summary>
|
||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||
/// </summary>
|
||||
public WorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
|
||||
|
||||
var decoder = Decoder.GetDecoder<Beatmap>(sr);
|
||||
IBeatmap beatmap = decoder.Decode(sr);
|
||||
/// <summary>
|
||||
/// Fired when a notification should be presented to the user.
|
||||
/// </summary>
|
||||
public Action<Notification> PostNotification { set => beatmapModelManager.PostNotification = value; }
|
||||
|
||||
string hash = ms.ComputeSHA2Hash();
|
||||
/// <summary>
|
||||
/// Fired when the user requests to view the resulting import.
|
||||
/// </summary>
|
||||
public Action<IEnumerable<BeatmapSetInfo>> PresentImport { set => beatmapModelManager.PresentImport = value; }
|
||||
|
||||
if (beatmapInfos.Any(b => b.Hash == hash))
|
||||
continue;
|
||||
/// <summary>
|
||||
/// Delete a beatmap difficulty.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap difficulty to hide.</param>
|
||||
public void Hide(BeatmapInfo beatmap) => beatmapModelManager.Hide(beatmap);
|
||||
|
||||
beatmap.BeatmapInfo.Path = file.Filename;
|
||||
beatmap.BeatmapInfo.Hash = hash;
|
||||
beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash();
|
||||
|
||||
var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
|
||||
beatmap.BeatmapInfo.Ruleset = ruleset;
|
||||
|
||||
// TODO: this should be done in a better place once we actually need to dynamically update it.
|
||||
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
|
||||
beatmap.BeatmapInfo.Length = calculateLength(beatmap);
|
||||
beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength();
|
||||
|
||||
beatmapInfos.Add(beatmap.BeatmapInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return beatmapInfos;
|
||||
}
|
||||
|
||||
private double calculateLength(IBeatmap b)
|
||||
{
|
||||
if (!b.HitObjects.Any())
|
||||
return 0;
|
||||
|
||||
var lastObject = b.HitObjects.Last();
|
||||
|
||||
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
|
||||
double endTime = lastObject.GetEndTime();
|
||||
double startTime = b.HitObjects.First().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);
|
||||
/// <summary>
|
||||
/// Restore a beatmap difficulty.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap difficulty to restore.</param>
|
||||
public void Restore(BeatmapInfo beatmap) => beatmapModelManager.Restore(beatmap);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
|
||||
/// </summary>
|
||||
private class DummyConversionBeatmap : WorkingBeatmap
|
||||
#region Implementation of IModelManager<BeatmapSetInfo>
|
||||
|
||||
public IBindable<WeakReference<BeatmapSetInfo>> ItemUpdated => beatmapModelManager.ItemUpdated;
|
||||
|
||||
public IBindable<WeakReference<BeatmapSetInfo>> ItemRemoved => beatmapModelManager.ItemRemoved;
|
||||
|
||||
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||
{
|
||||
private readonly IBeatmap beatmap;
|
||||
|
||||
public DummyConversionBeatmap(IBeatmap beatmap)
|
||||
: base(beatmap.BeatmapInfo, null)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override IBeatmap GetBeatmap() => beatmap;
|
||||
protected override Texture GetBackground() => null;
|
||||
protected override Track GetBeatmapTrack() => null;
|
||||
protected internal override ISkin GetSkin() => null;
|
||||
public override Stream GetStream(string storagePath) => null;
|
||||
return beatmapModelManager.ImportFromStableAsync(stableStorage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The level of detail to include in database results.
|
||||
/// </summary>
|
||||
public enum IncludedDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Only include beatmap difficulties and set level metadata.
|
||||
/// </summary>
|
||||
Minimal,
|
||||
public void Export(BeatmapSetInfo item)
|
||||
{
|
||||
beatmapModelManager.Export(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Include all difficulties, rulesets, difficulty metadata but no files.
|
||||
/// </summary>
|
||||
AllButFiles,
|
||||
public void ExportModelTo(BeatmapSetInfo model, Stream outputStream)
|
||||
{
|
||||
beatmapModelManager.ExportModelTo(model, outputStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap.
|
||||
/// </summary>
|
||||
AllButRuleset,
|
||||
public void Update(BeatmapSetInfo item)
|
||||
{
|
||||
beatmapModelManager.Update(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Include everything.
|
||||
/// </summary>
|
||||
All
|
||||
public bool Delete(BeatmapSetInfo item)
|
||||
{
|
||||
return beatmapModelManager.Delete(item);
|
||||
}
|
||||
|
||||
public void Delete(List<BeatmapSetInfo> items, bool silent = false)
|
||||
{
|
||||
beatmapModelManager.Delete(items, silent);
|
||||
}
|
||||
|
||||
public void Undelete(List<BeatmapSetInfo> items, bool silent = false)
|
||||
{
|
||||
beatmapModelManager.Undelete(items, silent);
|
||||
}
|
||||
|
||||
public void Undelete(BeatmapSetInfo item)
|
||||
{
|
||||
beatmapModelManager.Undelete(item);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IModelDownloader<BeatmapSetInfo>
|
||||
|
||||
public IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> DownloadBegan => beatmapModelManager.DownloadBegan;
|
||||
|
||||
public IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> DownloadFailed => beatmapModelManager.DownloadFailed;
|
||||
|
||||
public bool IsAvailableLocally(BeatmapSetInfo model)
|
||||
{
|
||||
return beatmapModelManager.IsAvailableLocally(model);
|
||||
}
|
||||
|
||||
public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false)
|
||||
{
|
||||
return beatmapModelManager.Download(model, minimiseDownloadSize);
|
||||
}
|
||||
|
||||
public ArchiveDownloadRequest<BeatmapSetInfo> GetExistingDownload(BeatmapSetInfo model)
|
||||
{
|
||||
return beatmapModelManager.GetExistingDownload(model);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of ICanAcceptFiles
|
||||
|
||||
public Task Import(params string[] paths)
|
||||
{
|
||||
return beatmapModelManager.Import(paths);
|
||||
}
|
||||
|
||||
public Task Import(params ImportTask[] tasks)
|
||||
{
|
||||
return beatmapModelManager.Import(tasks);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<BeatmapSetInfo>> Import(ProgressNotification notification, params ImportTask[] tasks)
|
||||
{
|
||||
return beatmapModelManager.Import(notification, tasks);
|
||||
}
|
||||
|
||||
public Task<BeatmapSetInfo> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return beatmapModelManager.Import(task, lowPriority, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<BeatmapSetInfo> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return beatmapModelManager.Import(archive, lowPriority, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken);
|
||||
}
|
||||
|
||||
public IEnumerable<string> HandledExtensions => beatmapModelManager.HandledExtensions;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IWorkingBeatmapCache
|
||||
|
||||
public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IModelFileManager<in BeatmapSetInfo,in BeatmapSetFileInfo>
|
||||
|
||||
public void ReplaceFile(BeatmapSetInfo model, BeatmapSetFileInfo file, Stream contents, string filename = null)
|
||||
{
|
||||
beatmapModelManager.ReplaceFile(model, file, contents, filename);
|
||||
}
|
||||
|
||||
public void DeleteFile(BeatmapSetInfo model, BeatmapSetFileInfo file)
|
||||
{
|
||||
beatmapModelManager.DeleteFile(model, file);
|
||||
}
|
||||
|
||||
public void AddFile(BeatmapSetInfo model, Stream contents, string filename)
|
||||
{
|
||||
beatmapModelManager.AddFile(model, contents, filename);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
onlineBetamapLookupQueue?.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
478
osu.Game/Beatmaps/BeatmapModelManager.cs
Normal file
478
osu.Game/Beatmaps/BeatmapModelManager.cs
Normal file
@ -0,0 +1,478 @@
|
||||
// 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 System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Skinning;
|
||||
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles ef-core storage of beatmaps.
|
||||
/// </summary>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public class BeatmapModelManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been hidden.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapHidden => beatmapHidden;
|
||||
|
||||
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapHidden = new Bindable<WeakReference<BeatmapInfo>>();
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been restored.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapRestored;
|
||||
|
||||
/// <summary>
|
||||
/// An online lookup queue component which handles populating online beatmap metadata.
|
||||
/// </summary>
|
||||
public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The game working beatmap cache, used to invalidate entries on changes.
|
||||
/// </summary>
|
||||
public WorkingBeatmapCache WorkingBeatmapCache { private get; set; }
|
||||
|
||||
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapRestored = new Bindable<WeakReference<BeatmapInfo>>();
|
||||
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
|
||||
protected override string ImportFromStablePath => ".";
|
||||
|
||||
protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage();
|
||||
|
||||
private readonly BeatmapStore beatmaps;
|
||||
private readonly RulesetStore rulesets;
|
||||
|
||||
public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null)
|
||||
: base(storage, contextFactory, api, new BeatmapStore(contextFactory), host)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
|
||||
beatmaps = (BeatmapStore)ModelStore;
|
||||
beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference<BeatmapInfo>(b);
|
||||
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b);
|
||||
beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b);
|
||||
beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj);
|
||||
}
|
||||
|
||||
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
|
||||
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
|
||||
|
||||
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
|
||||
|
||||
protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (archive != null)
|
||||
beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files);
|
||||
|
||||
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
|
||||
{
|
||||
// remove metadata from difficulties where it matches the set
|
||||
if (beatmapSet.Metadata.Equals(b.Metadata))
|
||||
b.Metadata = null;
|
||||
|
||||
b.BeatmapSet = beatmapSet;
|
||||
}
|
||||
|
||||
validateOnlineIds(beatmapSet);
|
||||
|
||||
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
|
||||
|
||||
if (OnlineLookupQueue != null)
|
||||
await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
||||
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
|
||||
{
|
||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||
{
|
||||
beatmapSet.OnlineBeatmapSetID = null;
|
||||
LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PreImport(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
|
||||
throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
|
||||
|
||||
// check if a set already exists with the same online id, delete if it does.
|
||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||
{
|
||||
var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
|
||||
|
||||
if (existingOnlineId != null)
|
||||
{
|
||||
Delete(existingOnlineId);
|
||||
|
||||
// in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
|
||||
existingOnlineId.OnlineBeatmapSetID = null;
|
||||
foreach (var b in existingOnlineId.Beatmaps)
|
||||
b.OnlineBeatmapID = null;
|
||||
|
||||
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOnlineIds(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
|
||||
|
||||
// ensure all IDs are unique
|
||||
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
|
||||
{
|
||||
LogForModel(beatmapSet, "Found non-unique IDs, resetting...");
|
||||
resetIds();
|
||||
return;
|
||||
}
|
||||
|
||||
// find any existing beatmaps in the database that have matching online ids
|
||||
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList();
|
||||
|
||||
if (existingBeatmaps.Count > 0)
|
||||
{
|
||||
// reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
|
||||
// we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
|
||||
var existing = CheckForExisting(beatmapSet);
|
||||
|
||||
if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b)))
|
||||
{
|
||||
LogForModel(beatmapSet, "Found existing import with IDs already, resetting...");
|
||||
resetIds();
|
||||
}
|
||||
}
|
||||
|
||||
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
|
||||
}
|
||||
|
||||
protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable<BeatmapSetInfo> items)
|
||||
=> base.CheckLocalAvailability(model, items)
|
||||
|| (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID));
|
||||
|
||||
/// <summary>
|
||||
/// Delete a beatmap difficulty.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap difficulty to hide.</param>
|
||||
public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap);
|
||||
|
||||
/// <summary>
|
||||
/// Restore a beatmap difficulty.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap difficulty to restore.</param>
|
||||
public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap);
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
||||
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
||||
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
||||
public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null)
|
||||
{
|
||||
var setInfo = info.BeatmapSet;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID);
|
||||
var metadata = beatmapInfo.Metadata ?? setInfo.Metadata;
|
||||
|
||||
// grab the original file (or create a new one if not found).
|
||||
var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo();
|
||||
|
||||
// metadata may have changed; update the path with the standard format.
|
||||
beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu";
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
|
||||
// update existing or populate new file's filename.
|
||||
fileInfo.Filename = beatmapInfo.Path;
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
ReplaceFile(setInfo, fileInfo, stream);
|
||||
}
|
||||
}
|
||||
|
||||
WorkingBeatmapCache?.Invalidate(info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
|
||||
|
||||
protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||
{
|
||||
if (!base.CanSkipImport(existing, import))
|
||||
return false;
|
||||
|
||||
return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null);
|
||||
}
|
||||
|
||||
protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||
{
|
||||
if (!base.CanReuseExisting(existing, import))
|
||||
return false;
|
||||
|
||||
var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
|
||||
var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
|
||||
|
||||
// force re-import if we are not in a sane state.
|
||||
return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||
public List<BeatmapSetInfo> GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) =>
|
||||
GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. Note that files are not populated.
|
||||
/// </summary>
|
||||
/// <param name="includes">The level of detail to include in the returned objects.</param>
|
||||
/// <param name="includeProtected">Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases.</param>
|
||||
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||
public IEnumerable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false)
|
||||
{
|
||||
IQueryable<BeatmapSetInfo> queryable;
|
||||
|
||||
switch (includes)
|
||||
{
|
||||
case IncludedDetails.Minimal:
|
||||
queryable = beatmaps.BeatmapSetsOverview;
|
||||
break;
|
||||
|
||||
case IncludedDetails.AllButRuleset:
|
||||
queryable = beatmaps.BeatmapSetsWithoutRuleset;
|
||||
break;
|
||||
|
||||
case IncludedDetails.AllButFiles:
|
||||
queryable = beatmaps.BeatmapSetsWithoutFiles;
|
||||
break;
|
||||
|
||||
default:
|
||||
queryable = beatmaps.ConsumableItems;
|
||||
break;
|
||||
}
|
||||
|
||||
// AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY
|
||||
// clause which causes queries to take 5-10x longer.
|
||||
// TODO: remove if upgrading to EF core 3.x.
|
||||
return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="includes">The level of detail to include in the returned objects.</param>
|
||||
/// <returns>Results from the provided query.</returns>
|
||||
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query, IncludedDetails includes = IncludedDetails.All)
|
||||
{
|
||||
IQueryable<BeatmapSetInfo> queryable;
|
||||
|
||||
switch (includes)
|
||||
{
|
||||
case IncludedDetails.Minimal:
|
||||
queryable = beatmaps.BeatmapSetsOverview;
|
||||
break;
|
||||
|
||||
case IncludedDetails.AllButRuleset:
|
||||
queryable = beatmaps.BeatmapSetsWithoutRuleset;
|
||||
break;
|
||||
|
||||
case IncludedDetails.AllButFiles:
|
||||
queryable = beatmaps.BeatmapSetsWithoutFiles;
|
||||
break;
|
||||
|
||||
default:
|
||||
queryable = beatmaps.ConsumableItems;
|
||||
break;
|
||||
}
|
||||
|
||||
return queryable.AsNoTracking().Where(query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapInfo QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>Results from the provided query.</returns>
|
||||
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
|
||||
|
||||
protected override string HumanisedModelName => "beatmap";
|
||||
|
||||
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
|
||||
{
|
||||
// let's make sure there are actually .osu files to import.
|
||||
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (string.IsNullOrEmpty(mapName))
|
||||
{
|
||||
Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database);
|
||||
return null;
|
||||
}
|
||||
|
||||
Beatmap beatmap;
|
||||
using (var stream = new LineBufferedReader(reader.GetStream(mapName)))
|
||||
beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
|
||||
|
||||
return new BeatmapSetInfo
|
||||
{
|
||||
OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
|
||||
Beatmaps = new List<BeatmapInfo>(),
|
||||
Metadata = beatmap.Metadata,
|
||||
DateAdded = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
|
||||
/// </summary>
|
||||
private List<BeatmapInfo> createBeatmapDifficulties(List<BeatmapSetFileInfo> files)
|
||||
{
|
||||
var beatmapInfos = new List<BeatmapInfo>();
|
||||
|
||||
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
|
||||
using (var ms = new MemoryStream()) // we need a memory stream so we can seek
|
||||
using (var sr = new LineBufferedReader(ms))
|
||||
{
|
||||
raw.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
var decoder = Decoder.GetDecoder<Beatmap>(sr);
|
||||
IBeatmap beatmap = decoder.Decode(sr);
|
||||
|
||||
string hash = ms.ComputeSHA2Hash();
|
||||
|
||||
if (beatmapInfos.Any(b => b.Hash == hash))
|
||||
continue;
|
||||
|
||||
beatmap.BeatmapInfo.Path = file.Filename;
|
||||
beatmap.BeatmapInfo.Hash = hash;
|
||||
beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash();
|
||||
|
||||
var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
|
||||
beatmap.BeatmapInfo.Ruleset = ruleset;
|
||||
|
||||
// TODO: this should be done in a better place once we actually need to dynamically update it.
|
||||
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
|
||||
beatmap.BeatmapInfo.Length = calculateLength(beatmap);
|
||||
beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength();
|
||||
|
||||
beatmapInfos.Add(beatmap.BeatmapInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return beatmapInfos;
|
||||
}
|
||||
|
||||
private double calculateLength(IBeatmap b)
|
||||
{
|
||||
if (!b.HitObjects.Any())
|
||||
return 0;
|
||||
|
||||
var lastObject = b.HitObjects.Last();
|
||||
|
||||
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
|
||||
double endTime = lastObject.GetEndTime();
|
||||
double startTime = b.HitObjects.First().StartTime;
|
||||
|
||||
return endTime - startTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
|
||||
/// </summary>
|
||||
private class DummyConversionBeatmap : WorkingBeatmap
|
||||
{
|
||||
private readonly IBeatmap beatmap;
|
||||
|
||||
public DummyConversionBeatmap(IBeatmap beatmap)
|
||||
: base(beatmap.BeatmapInfo, null)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override IBeatmap GetBeatmap() => beatmap;
|
||||
protected override Texture GetBackground() => null;
|
||||
protected override Track GetBeatmapTrack() => null;
|
||||
protected internal override ISkin GetSkin() => null;
|
||||
public override Stream GetStream(string storagePath) => null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The level of detail to include in database results.
|
||||
/// </summary>
|
||||
public enum IncludedDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// Only include beatmap difficulties and set level metadata.
|
||||
/// </summary>
|
||||
Minimal,
|
||||
|
||||
/// <summary>
|
||||
/// Include all difficulties, rulesets, difficulty metadata but no files.
|
||||
/// </summary>
|
||||
AllButFiles,
|
||||
|
||||
/// <summary>
|
||||
/// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap.
|
||||
/// </summary>
|
||||
AllButRuleset,
|
||||
|
||||
/// <summary>
|
||||
/// Include everything.
|
||||
/// </summary>
|
||||
All
|
||||
}
|
||||
}
|
15
osu.Game/Beatmaps/IWorkingBeatmapCache.cs
Normal file
15
osu.Game/Beatmaps/IWorkingBeatmapCache.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public interface IWorkingBeatmapCache
|
||||
{
|
||||
/// <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>
|
||||
WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo);
|
||||
}
|
||||
}
|
@ -1,12 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// 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 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;
|
||||
@ -15,8 +21,96 @@ using osu.Game.Storyboards;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public partial class BeatmapManager
|
||||
public class WorkingBeatmapCache : IBeatmapResourceProvider, IWorkingBeatmapCache
|
||||
{
|
||||
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 BeatmapModelManager 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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
@ -30,7 +30,7 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The model type.</typeparam>
|
||||
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
|
||||
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>
|
||||
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
|
||||
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
|
||||
where TFileModel : class, INamedFileInfo, new()
|
||||
{
|
||||
@ -135,7 +135,7 @@ namespace osu.Game.Database
|
||||
return Import(notification, tasks);
|
||||
}
|
||||
|
||||
protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params ImportTask[] tasks)
|
||||
public async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params ImportTask[] tasks)
|
||||
{
|
||||
if (tasks.Length == 0)
|
||||
{
|
||||
@ -227,7 +227,7 @@ namespace osu.Game.Database
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
/// <returns>The imported model, if successful.</returns>
|
||||
internal async Task<TModel> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
public async Task<TModel> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@ -479,7 +479,7 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
/// <param name="model">The item to export.</param>
|
||||
/// <param name="outputStream">The output stream to export to.</param>
|
||||
protected virtual void ExportModelTo(TModel model, Stream outputStream)
|
||||
public virtual void ExportModelTo(TModel model, Stream outputStream)
|
||||
{
|
||||
using (var archive = ZipArchive.Create())
|
||||
{
|
||||
@ -745,9 +745,6 @@ namespace osu.Game.Database
|
||||
/// <returns>Whether to perform deletion.</returns>
|
||||
protected virtual bool ShouldDeleteArchive(string path) => false;
|
||||
|
||||
/// <summary>
|
||||
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
|
||||
/// </summary>
|
||||
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||
{
|
||||
var storage = PrepareStableStorage(stableStorage);
|
||||
|
@ -54,12 +54,6 @@ namespace osu.Game.Database
|
||||
/// <returns>The request object.</returns>
|
||||
protected abstract ArchiveDownloadRequest<TModel> CreateDownloadRequest(TModel model, bool minimiseDownloadSize);
|
||||
|
||||
/// <summary>
|
||||
/// Begin a download for the requested <typeparamref name="TModel"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <typeparamref name="TModel"/> to be downloaded.</param>
|
||||
/// <param name="minimiseDownloadSize">Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle.</param>
|
||||
/// <returns>Whether the download was started.</returns>
|
||||
public bool Download(TModel model, bool minimiseDownloadSize = false)
|
||||
{
|
||||
if (!canDownload(model)) return false;
|
||||
|
36
osu.Game/Database/IModelFileManager.cs
Normal file
36
osu.Game/Database/IModelFileManager.cs
Normal file
@ -0,0 +1,36 @@
|
||||
// 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.IO;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public interface IModelFileManager<in TModel, in TFileModel>
|
||||
where TModel : class
|
||||
where TFileModel : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Replace an existing file with a new version.
|
||||
/// </summary>
|
||||
/// <param name="model">The item to operate on.</param>
|
||||
/// <param name="file">The existing file to be replaced.</param>
|
||||
/// <param name="contents">The new file contents.</param>
|
||||
/// <param name="filename">An optional filename for the new file. Will use the previous filename if not specified.</param>
|
||||
void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null);
|
||||
|
||||
/// <summary>
|
||||
/// Delete an existing file.
|
||||
/// </summary>
|
||||
/// <param name="model">The item to operate on.</param>
|
||||
/// <param name="file">The existing file to be deleted.</param>
|
||||
void DeleteFile(TModel model, TFileModel file);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new file.
|
||||
/// </summary>
|
||||
/// <param name="model">The item to operate on.</param>
|
||||
/// <param name="contents">The new file contents.</param>
|
||||
/// <param name="filename">The filename for the new file.</param>
|
||||
void AddFile(TModel model, Stream contents, string filename);
|
||||
}
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
@ -24,5 +31,97 @@ namespace osu.Game.Database
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// </summary>
|
||||
IBindable<WeakReference<TModel>> ItemRemoved { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
|
||||
/// </summary>
|
||||
Task ImportFromStableAsync(StableStorage stableStorage);
|
||||
|
||||
/// <summary>
|
||||
/// Exports an item to a legacy (.zip based) package.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to export.</param>
|
||||
void Export(TModel item);
|
||||
|
||||
/// <summary>
|
||||
/// Exports an item to the given output stream.
|
||||
/// </summary>
|
||||
/// <param name="model">The item to export.</param>
|
||||
/// <param name="outputStream">The output stream to export to.</param>
|
||||
void ExportModelTo(TModel model, Stream outputStream);
|
||||
|
||||
/// <summary>
|
||||
/// Perform an update of the specified item.
|
||||
/// TODO: Support file additions/removals.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to update.</param>
|
||||
void Update(TModel item);
|
||||
|
||||
/// <summary>
|
||||
/// Delete an item from the manager.
|
||||
/// Is a no-op for already deleted items.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to delete.</param>
|
||||
/// <returns>false if no operation was performed</returns>
|
||||
bool Delete(TModel item);
|
||||
|
||||
/// <summary>
|
||||
/// Delete multiple items.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
void Delete(List<TModel> items, bool silent = false);
|
||||
|
||||
/// <summary>
|
||||
/// Restore multiple items that were previously deleted.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
void Undelete(List<TModel> items, bool silent = false);
|
||||
|
||||
/// <summary>
|
||||
/// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to restore</param>
|
||||
void Undelete(TModel item);
|
||||
|
||||
/// <summary>
|
||||
/// Import one or more <typeparamref name="TModel"/> items from filesystem <paramref name="paths"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will be treated as a low priority import if more than one path is specified; use <see cref="ArchiveModelManager{TModel,TFileModel}.Import(osu.Game.Database.ImportTask[])"/> to always import at standard priority.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </remarks>
|
||||
/// <param name="paths">One or more archive locations on disk.</param>
|
||||
Task Import(params string[] paths);
|
||||
|
||||
Task Import(params ImportTask[] tasks);
|
||||
|
||||
Task<IEnumerable<TModel>> Import(ProgressNotification notification, params ImportTask[] tasks);
|
||||
|
||||
/// <summary>
|
||||
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
|
||||
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
||||
/// </summary>
|
||||
/// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
/// <returns>The imported model, if successful.</returns>
|
||||
Task<TModel> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Silently import an item from an <see cref="ArchiveReader"/>.
|
||||
/// </summary>
|
||||
/// <param name="archive">The archive to be imported.</param>
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
Task<TModel> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Silently import an item from a <typeparamref name="TModel"/>.
|
||||
/// </summary>
|
||||
/// <param name="item">The model to be imported.</param>
|
||||
/// <param name="archive">An optional archive to use for model population.</param>
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
Task<TModel> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
@ -138,8 +138,6 @@ namespace osu.Game
|
||||
|
||||
private UserLookupCache userCache;
|
||||
|
||||
private BeatmapOnlineLookupQueue onlineBeatmapLookupQueue;
|
||||
|
||||
private FileStore fileStore;
|
||||
|
||||
private RulesetConfigCache rulesetConfigCache;
|
||||
@ -244,11 +242,7 @@ namespace osu.Game
|
||||
|
||||
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
|
||||
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig));
|
||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap));
|
||||
|
||||
onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(API, Storage);
|
||||
|
||||
BeatmapManager.OnlineLookupQueue = onlineBeatmapLookupQueue;
|
||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
|
||||
|
||||
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary
|
||||
// to have inter-dependent model managers. this could be obtained with an IHasForeign<T> interface to
|
||||
@ -530,8 +524,8 @@ namespace osu.Game
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
RulesetStore?.Dispose();
|
||||
BeatmapManager?.Dispose();
|
||||
LocalConfig?.Dispose();
|
||||
onlineBeatmapLookupQueue?.Dispose();
|
||||
|
||||
contextFactory?.FlushConnections();
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Scoring
|
||||
protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
protected override void ExportModelTo(ScoreInfo model, Stream outputStream)
|
||||
public override void ExportModelTo(ScoreInfo model, Stream outputStream)
|
||||
{
|
||||
var file = model.Files.SingleOrDefault();
|
||||
if (file == null)
|
||||
|
@ -123,11 +123,40 @@ namespace osu.Game.Tests.Visual
|
||||
this.testBeatmap = testBeatmap;
|
||||
}
|
||||
|
||||
protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null)
|
||||
=> string.Empty;
|
||||
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host)
|
||||
{
|
||||
return new TestBeatmapModelManager(storage, contextFactory, rulesets, api, host);
|
||||
}
|
||||
|
||||
public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
|
||||
=> testBeatmap;
|
||||
protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
|
||||
{
|
||||
return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host);
|
||||
}
|
||||
|
||||
private class TestWorkingBeatmapCache : WorkingBeatmapCache
|
||||
{
|
||||
private readonly TestBeatmapManager testBeatmapManager;
|
||||
|
||||
public TestWorkingBeatmapCache(TestBeatmapManager testBeatmapManager, AudioManager audioManager, IResourceStore<byte[]> resourceStore, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost gameHost)
|
||||
: base(audioManager, resourceStore, storage, defaultBeatmap, gameHost)
|
||||
{
|
||||
this.testBeatmapManager = testBeatmapManager;
|
||||
}
|
||||
|
||||
public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
|
||||
=> testBeatmapManager.testBeatmap;
|
||||
}
|
||||
|
||||
internal class TestBeatmapModelManager : BeatmapModelManager
|
||||
{
|
||||
public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost)
|
||||
: base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null)
|
||||
=> string.Empty;
|
||||
}
|
||||
|
||||
public override void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user