mirror of
https://github.com/ppy/osu
synced 2025-03-23 19:36:56 +00: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; }
|
public Task<BeatmapSetInfo> CurrentImportTask { get; private set; }
|
||||||
|
|
||||||
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize)
|
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host)
|
||||||
=> new TestDownloadRequest(set);
|
{
|
||||||
|
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)
|
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)
|
: 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);
|
private readonly TestBeatmapManager testBeatmapManager;
|
||||||
return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false);
|
|
||||||
|
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.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Audio.Track;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Framework.Graphics.Textures;
|
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Lists;
|
|
||||||
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.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.IO.Archives;
|
using osu.Game.IO.Archives;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
|
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
|
/// Handles general operations related to global beatmap management.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
public partial class BeatmapManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>, IBeatmapResourceProvider
|
public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly BeatmapModelManager beatmapModelManager;
|
||||||
/// Fired when a single difficulty has been hidden.
|
private readonly WorkingBeatmapCache workingBeatmapCache;
|
||||||
/// </summary>
|
private readonly BeatmapOnlineLookupQueue onlineBetamapLookupQueue;
|
||||||
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;
|
|
||||||
|
|
||||||
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null,
|
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null,
|
||||||
WorkingBeatmap defaultBeatmap = null)
|
WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
|
||||||
: base(storage, contextFactory, api, new BeatmapStore(contextFactory), host)
|
|
||||||
{
|
{
|
||||||
this.rulesets = rulesets;
|
beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host);
|
||||||
this.audioManager = audioManager;
|
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host);
|
||||||
this.resources = resources;
|
|
||||||
this.host = host;
|
|
||||||
|
|
||||||
DefaultBeatmap = defaultBeatmap;
|
workingBeatmapCache.BeatmapManager = beatmapModelManager;
|
||||||
|
|
||||||
beatmaps = (BeatmapStore)ModelStore;
|
if (performOnlineLookups)
|
||||||
beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference<BeatmapInfo>(b);
|
{
|
||||||
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b);
|
onlineBetamapLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
|
||||||
beatmaps.ItemRemoved += removeWorkingCache;
|
beatmapModelManager.OnlineLookupQueue = onlineBetamapLookupQueue;
|
||||||
beatmaps.ItemUpdated += removeWorkingCache;
|
}
|
||||||
|
|
||||||
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store));
|
|
||||||
trackStore = audioManager.GetTrackStore(Files.Store);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
|
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host) =>
|
||||||
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
|
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)
|
public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user)
|
||||||
{
|
{
|
||||||
var metadata = new BeatmapMetadata
|
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());
|
return GetWorkingBeatmap(working.Beatmaps.First());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
|
#region Delegation to BeatmapModelManager (methods which previously existed locally).
|
||||||
{
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Delete a beatmap difficulty.
|
/// Fired when a single difficulty has been hidden.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="beatmap">The beatmap difficulty to hide.</param>
|
public IBindable<WeakReference<BeatmapInfo>> BeatmapHidden => beatmapModelManager.BeatmapHidden;
|
||||||
public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Restore a beatmap difficulty.
|
/// Fired when a single difficulty has been restored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="beatmap">The beatmap difficulty to restore.</param>
|
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapModelManager.BeatmapRestored;
|
||||||
public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
/// 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="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="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>
|
/// <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)
|
public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) => beatmapModelManager.Save(info, beatmapContent, beatmapSkin);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
|
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||||
public List<BeatmapSetInfo> GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) =>
|
public List<BeatmapSetInfo> GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includes, includeProtected);
|
||||||
GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s. Note that files are not populated.
|
/// 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="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>
|
/// <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>
|
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
|
||||||
public IEnumerable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false)
|
public IEnumerable<BeatmapSetInfo> GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includes, includeProtected);
|
||||||
{
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
/// 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="query">The query.</param>
|
||||||
/// <param name="includes">The level of detail to include in the returned objects.</param>
|
/// <param name="includes">The level of detail to include in the returned objects.</param>
|
||||||
/// <returns>Results from the provided query.</returns>
|
/// <returns>Results from the provided query.</returns>
|
||||||
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query, IncludedDetails includes = IncludedDetails.All)
|
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query, IncludedDetails includes = IncludedDetails.All) => beatmapModelManager.QueryBeatmapSets(query, includes);
|
||||||
{
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The query.</param>
|
/// <param name="query">The query.</param>
|
||||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The query.</param>
|
/// <param name="query">The query.</param>
|
||||||
/// <returns>Results from the provided query.</returns>
|
/// <returns>Results from the provided query.</returns>
|
||||||
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
|
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmapModelManager.QueryBeatmaps(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>
|
/// <summary>
|
||||||
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
|
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private List<BeatmapInfo> createBeatmapDifficulties(List<BeatmapSetFileInfo> files)
|
/// <param name="query">The query.</param>
|
||||||
{
|
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||||
var beatmapInfos = new List<BeatmapInfo>();
|
public BeatmapInfo QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => beatmapModelManager.QueryBeatmap(query);
|
||||||
|
|
||||||
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
|
/// <summary>
|
||||||
{
|
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||||
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
|
/// </summary>
|
||||||
using (var ms = new MemoryStream()) // we need a memory stream so we can seek
|
public WorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
|
||||||
using (var sr = new LineBufferedReader(ms))
|
|
||||||
{
|
|
||||||
raw.CopyTo(ms);
|
|
||||||
ms.Position = 0;
|
|
||||||
|
|
||||||
var decoder = Decoder.GetDecoder<Beatmap>(sr);
|
/// <summary>
|
||||||
IBeatmap beatmap = decoder.Decode(sr);
|
/// 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))
|
/// <summary>
|
||||||
continue;
|
/// 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;
|
/// <summary>
|
||||||
beatmap.BeatmapInfo.Hash = hash;
|
/// Restore a beatmap difficulty.
|
||||||
beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash();
|
/// </summary>
|
||||||
|
/// <param name="beatmap">The beatmap difficulty to restore.</param>
|
||||||
var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
|
public void Restore(BeatmapInfo beatmap) => beatmapModelManager.Restore(beatmap);
|
||||||
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);
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
#region Implementation of IModelManager<BeatmapSetInfo>
|
||||||
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
|
|
||||||
/// </summary>
|
public IBindable<WeakReference<BeatmapSetInfo>> ItemUpdated => beatmapModelManager.ItemUpdated;
|
||||||
private class DummyConversionBeatmap : WorkingBeatmap
|
|
||||||
|
public IBindable<WeakReference<BeatmapSetInfo>> ItemRemoved => beatmapModelManager.ItemRemoved;
|
||||||
|
|
||||||
|
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||||
{
|
{
|
||||||
private readonly IBeatmap beatmap;
|
return beatmapModelManager.ImportFromStableAsync(stableStorage);
|
||||||
|
|
||||||
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>
|
public void Export(BeatmapSetInfo item)
|
||||||
/// The level of detail to include in database results.
|
{
|
||||||
/// </summary>
|
beatmapModelManager.Export(item);
|
||||||
public enum IncludedDetails
|
}
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Only include beatmap difficulties and set level metadata.
|
|
||||||
/// </summary>
|
|
||||||
Minimal,
|
|
||||||
|
|
||||||
/// <summary>
|
public void ExportModelTo(BeatmapSetInfo model, Stream outputStream)
|
||||||
/// Include all difficulties, rulesets, difficulty metadata but no files.
|
{
|
||||||
/// </summary>
|
beatmapModelManager.ExportModelTo(model, outputStream);
|
||||||
AllButFiles,
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void Update(BeatmapSetInfo item)
|
||||||
/// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap.
|
{
|
||||||
/// </summary>
|
beatmapModelManager.Update(item);
|
||||||
AllButRuleset,
|
}
|
||||||
|
|
||||||
/// <summary>
|
public bool Delete(BeatmapSetInfo item)
|
||||||
/// Include everything.
|
{
|
||||||
/// </summary>
|
return beatmapModelManager.Delete(item);
|
||||||
All
|
}
|
||||||
|
|
||||||
|
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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
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.Statistics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps.Formats;
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
@ -15,8 +21,96 @@ using osu.Game.Storyboards;
|
|||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
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]
|
[ExcludeFromDynamicCompile]
|
||||||
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
|
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
|
||||||
{
|
{
|
@ -30,7 +30,7 @@ namespace osu.Game.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TModel">The model type.</typeparam>
|
/// <typeparam name="TModel">The model type.</typeparam>
|
||||||
/// <typeparam name="TFileModel">The associated file join 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 TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
|
||||||
where TFileModel : class, INamedFileInfo, new()
|
where TFileModel : class, INamedFileInfo, new()
|
||||||
{
|
{
|
||||||
@ -135,7 +135,7 @@ namespace osu.Game.Database
|
|||||||
return Import(notification, tasks);
|
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)
|
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="lowPriority">Whether this is a low priority import.</param>
|
||||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||||
/// <returns>The imported model, if successful.</returns>
|
/// <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();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@ -479,7 +479,7 @@ namespace osu.Game.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="model">The item to export.</param>
|
/// <param name="model">The item to export.</param>
|
||||||
/// <param name="outputStream">The output stream to export to.</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())
|
using (var archive = ZipArchive.Create())
|
||||||
{
|
{
|
||||||
@ -745,9 +745,6 @@ namespace osu.Game.Database
|
|||||||
/// <returns>Whether to perform deletion.</returns>
|
/// <returns>Whether to perform deletion.</returns>
|
||||||
protected virtual bool ShouldDeleteArchive(string path) => false;
|
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)
|
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||||
{
|
{
|
||||||
var storage = PrepareStableStorage(stableStorage);
|
var storage = PrepareStableStorage(stableStorage);
|
||||||
|
@ -54,12 +54,6 @@ namespace osu.Game.Database
|
|||||||
/// <returns>The request object.</returns>
|
/// <returns>The request object.</returns>
|
||||||
protected abstract ArchiveDownloadRequest<TModel> CreateDownloadRequest(TModel model, bool minimiseDownloadSize);
|
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)
|
public bool Download(TModel model, bool minimiseDownloadSize = false)
|
||||||
{
|
{
|
||||||
if (!canDownload(model)) return 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
namespace osu.Game.Database
|
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.
|
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IBindable<WeakReference<TModel>> ItemRemoved { get; }
|
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 UserLookupCache userCache;
|
||||||
|
|
||||||
private BeatmapOnlineLookupQueue onlineBeatmapLookupQueue;
|
|
||||||
|
|
||||||
private FileStore fileStore;
|
private FileStore fileStore;
|
||||||
|
|
||||||
private RulesetConfigCache rulesetConfigCache;
|
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()
|
// 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(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));
|
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
|
||||||
|
|
||||||
onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(API, Storage);
|
|
||||||
|
|
||||||
BeatmapManager.OnlineLookupQueue = onlineBeatmapLookupQueue;
|
|
||||||
|
|
||||||
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary
|
// 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
|
// 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);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
RulesetStore?.Dispose();
|
RulesetStore?.Dispose();
|
||||||
|
BeatmapManager?.Dispose();
|
||||||
LocalConfig?.Dispose();
|
LocalConfig?.Dispose();
|
||||||
onlineBeatmapLookupQueue?.Dispose();
|
|
||||||
|
|
||||||
contextFactory?.FlushConnections();
|
contextFactory?.FlushConnections();
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Scoring
|
|||||||
protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
|
protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||||
=> Task.CompletedTask;
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
protected override void ExportModelTo(ScoreInfo model, Stream outputStream)
|
public override void ExportModelTo(ScoreInfo model, Stream outputStream)
|
||||||
{
|
{
|
||||||
var file = model.Files.SingleOrDefault();
|
var file = model.Files.SingleOrDefault();
|
||||||
if (file == null)
|
if (file == null)
|
||||||
|
@ -123,11 +123,40 @@ namespace osu.Game.Tests.Visual
|
|||||||
this.testBeatmap = testBeatmap;
|
this.testBeatmap = testBeatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null)
|
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host)
|
||||||
=> string.Empty;
|
{
|
||||||
|
return new TestBeatmapModelManager(storage, contextFactory, rulesets, api, host);
|
||||||
|
}
|
||||||
|
|
||||||
public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
|
protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
|
||||||
=> testBeatmap;
|
{
|
||||||
|
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)
|
public override void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user