Split out IModelDownloader and also split apart ScoreManager

This commit is contained in:
Dean Herbert 2021-09-30 18:21:16 +09:00
parent 0a00bc7795
commit 3e3b9bc963
15 changed files with 403 additions and 150 deletions

View File

@ -158,14 +158,30 @@ namespace osu.Game.Tests.Online
public Task<BeatmapSetInfo> CurrentImportTask { get; private set; } public Task<BeatmapSetInfo> CurrentImportTask { get; private set; }
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)
{
}
protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host)
{ {
return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, 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) protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
: base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap)
{ {
return new TestBeatmapModelDownloader(modelManager, api, host);
}
internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{
public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
: base(modelManager, apiProvider, gameHost)
{
}
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize)
=> new TestDownloadRequest(set);
} }
internal class TestBeatmapModelManager : BeatmapModelManager internal class TestBeatmapModelManager : BeatmapModelManager
@ -173,14 +189,11 @@ namespace osu.Game.Tests.Online
private readonly TestBeatmapManager testBeatmapManager; private readonly TestBeatmapManager testBeatmapManager;
public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost)
: base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) : base(storage, databaseContextFactory, rulesetStore, gameHost)
{ {
this.testBeatmapManager = testBeatmapManager; 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) public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{ {
await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); await testBeatmapManager.AllowImport.Task.ConfigureAwait(false);

View File

@ -32,12 +32,15 @@ namespace osu.Game.Beatmaps
public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, ICanAcceptFiles, IWorkingBeatmapCache public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, ICanAcceptFiles, IWorkingBeatmapCache
{ {
private readonly BeatmapModelManager beatmapModelManager; private readonly BeatmapModelManager beatmapModelManager;
private readonly BeatmapModelDownloader beatmapModelDownloader;
private readonly WorkingBeatmapCache workingBeatmapCache; private readonly WorkingBeatmapCache workingBeatmapCache;
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, bool performOnlineLookups = false) WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
{ {
beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host); beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host);
beatmapModelDownloader = CreateBeatmapModelDownloader(beatmapModelManager, api, host);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host);
workingBeatmapCache.BeatmapManager = beatmapModelManager; workingBeatmapCache.BeatmapManager = beatmapModelManager;
@ -49,11 +52,16 @@ namespace osu.Game.Beatmaps
} }
} }
protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
{
return new BeatmapModelDownloader(modelManager, api, host);
}
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host) => protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host) =>
new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host); new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host);
protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) => protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) =>
new BeatmapModelManager(storage, contextFactory, rulesets, api, host); new BeatmapModelManager(storage, contextFactory, rulesets, host);
/// <summary> /// <summary>
/// Create a new <see cref="WorkingBeatmap"/>. /// Create a new <see cref="WorkingBeatmap"/>.
@ -156,7 +164,14 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Fired when a notification should be presented to the user. /// Fired when a notification should be presented to the user.
/// </summary> /// </summary>
public Action<Notification> PostNotification { set => beatmapModelManager.PostNotification = value; } public Action<Notification> PostNotification
{
set
{
beatmapModelManager.PostNotification = value;
beatmapModelDownloader.PostNotification = value;
}
}
/// <summary> /// <summary>
/// Fired when the user requests to view the resulting import. /// Fired when the user requests to view the resulting import.
@ -179,6 +194,11 @@ namespace osu.Game.Beatmaps
#region Implementation of IModelManager<BeatmapSetInfo> #region Implementation of IModelManager<BeatmapSetInfo>
public bool IsAvailableLocally(BeatmapSetInfo model)
{
return beatmapModelManager.IsAvailableLocally(model);
}
public IBindable<WeakReference<BeatmapSetInfo>> ItemUpdated => beatmapModelManager.ItemUpdated; public IBindable<WeakReference<BeatmapSetInfo>> ItemUpdated => beatmapModelManager.ItemUpdated;
public IBindable<WeakReference<BeatmapSetInfo>> ItemRemoved => beatmapModelManager.ItemRemoved; public IBindable<WeakReference<BeatmapSetInfo>> ItemRemoved => beatmapModelManager.ItemRemoved;
@ -227,23 +247,18 @@ namespace osu.Game.Beatmaps
#region Implementation of IModelDownloader<BeatmapSetInfo> #region Implementation of IModelDownloader<BeatmapSetInfo>
public IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> DownloadBegan => beatmapModelManager.DownloadBegan; public IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> DownloadBegan => beatmapModelDownloader.DownloadBegan;
public IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> DownloadFailed => beatmapModelManager.DownloadFailed; public IBindable<WeakReference<ArchiveDownloadRequest<BeatmapSetInfo>>> DownloadFailed => beatmapModelDownloader.DownloadFailed;
public bool IsAvailableLocally(BeatmapSetInfo model)
{
return beatmapModelManager.IsAvailableLocally(model);
}
public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false) public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false)
{ {
return beatmapModelManager.Download(model, minimiseDownloadSize); return beatmapModelDownloader.Download(model, minimiseDownloadSize);
} }
public ArchiveDownloadRequest<BeatmapSetInfo> GetExistingDownload(BeatmapSetInfo model) public ArchiveDownloadRequest<BeatmapSetInfo> GetExistingDownload(BeatmapSetInfo model)
{ {
return beatmapModelManager.GetExistingDownload(model); return beatmapModelDownloader.GetExistingDownload(model);
} }
#endregion #endregion

View File

@ -0,0 +1,21 @@
// 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 osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
namespace osu.Game.Beatmaps
{
public class BeatmapModelDownloader : ModelDownloader<BeatmapSetInfo>
{
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null)
: base(beatmapModelManager, api, host)
{
}
}
}

View File

@ -21,8 +21,6 @@ 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.Requests;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -34,7 +32,7 @@ namespace osu.Game.Beatmaps
/// Handles ef-core storage of beatmaps. /// Handles ef-core storage of beatmaps.
/// </summary> /// </summary>
[ExcludeFromDynamicCompile] [ExcludeFromDynamicCompile]
public class BeatmapModelManager : DownloadableArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo> public class BeatmapModelManager : ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
{ {
/// <summary> /// <summary>
/// Fired when a single difficulty has been hidden. /// Fired when a single difficulty has been hidden.
@ -72,8 +70,8 @@ namespace osu.Game.Beatmaps
private readonly BeatmapStore beatmaps; private readonly BeatmapStore beatmaps;
private readonly RulesetStore rulesets; private readonly RulesetStore rulesets;
public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, GameHost host = null)
: base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) : base(storage, contextFactory, new BeatmapStore(contextFactory), host)
{ {
this.rulesets = rulesets; this.rulesets = rulesets;
@ -84,9 +82,6 @@ namespace osu.Game.Beatmaps
beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); 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 bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
@ -176,10 +171,6 @@ namespace osu.Game.Beatmaps
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); 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. /// Delete a beatmap difficulty.
/// </summary> /// </summary>
@ -347,7 +338,11 @@ namespace osu.Game.Beatmaps
/// <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) => beatmaps.Beatmaps.AsNoTracking().Where(query);
protected override string HumanisedModelName => "beatmap"; public override string HumanisedModelName => "beatmap";
protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable<BeatmapSetInfo> items)
=> base.CheckLocalAvailability(model, items)
|| (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID));
protected override BeatmapSetInfo CreateModel(ArchiveReader reader) protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{ {

View File

@ -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>, IModelFileManager<TModel, TFileModel> public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>, IModelFileManager<TModel, TFileModel>, IPresentImports<TModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : class, INamedFileInfo, new() where TFileModel : class, INamedFileInfo, new()
{ {
@ -249,10 +249,7 @@ namespace osu.Game.Database
return import; return import;
} }
/// <summary> public Action<IEnumerable<TModel>> PresentImport { protected get; set; }
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<TModel>> PresentImport;
/// <summary> /// <summary>
/// Silently import an item from an <see cref="ArchiveReader"/>. /// Silently import an item from an <see cref="ArchiveReader"/>.
@ -799,6 +796,17 @@ namespace osu.Game.Database
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns> /// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, ModelStore.ConsumableItems.Where(m => !m.DeletePending));
/// <summary>
/// Performs implementation specific comparisons to determine whether a given model is present in the local store.
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> whose existence needs to be checked.</param>
/// <param name="items">The usable items present in the store.</param>
/// <returns>Whether the <typeparamref name="TModel"/> exists.</returns>
protected virtual bool CheckLocalAvailability(TModel model, IQueryable<TModel> items)
=> model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any());
/// <summary> /// <summary>
/// Whether import can be skipped after finding an existing import early in the process. /// Whether import can be skipped after finding an existing import early in the process.
/// Only valid when <see cref="ComputeHash"/> is not overridden. /// Only valid when <see cref="ComputeHash"/> is not overridden.
@ -835,7 +843,7 @@ namespace osu.Game.Database
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>(); private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
#region Event handling / delaying #region Event handling / delaying

View File

@ -11,7 +11,7 @@ namespace osu.Game.Database
/// Represents a <see cref="IModelManager{TModel}"/> that can download new models from an external source. /// Represents a <see cref="IModelManager{TModel}"/> that can download new models from an external source.
/// </summary> /// </summary>
/// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TModel">The model type.</typeparam>
public interface IModelDownloader<TModel> : IModelManager<TModel> public interface IModelDownloader<TModel> : IPostNotifications
where TModel : class where TModel : class
{ {
/// <summary> /// <summary>
@ -26,13 +26,6 @@ namespace osu.Game.Database
/// </summary> /// </summary>
IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> DownloadFailed { get; } IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> DownloadFailed { get; }
/// <summary>
/// Checks whether a given <typeparamref name="TModel"/> is already available in the local store.
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> whose existence needs to be checked.</param>
/// <returns>Whether the <typeparamref name="TModel"/> exists.</returns>
bool IsAvailableLocally(TModel model);
/// <summary> /// <summary>
/// Begin a download for the requested <typeparamref name="TModel"/>. /// Begin a download for the requested <typeparamref name="TModel"/>.
/// </summary> /// </summary>

View File

@ -123,5 +123,17 @@ 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>
Task<TModel> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); Task<TModel> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default);
/// <summary>
/// Checks whether a given <typeparamref name="TModel"/> is already available in the local store.
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> whose existence needs to be checked.</param>
/// <returns>Whether the <typeparamref name="TModel"/> exists.</returns>
bool IsAvailableLocally(TModel model);
/// <summary>
/// A user displayable name for the model type associated with this manager.
/// </summary>
string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
} }
} }

View File

@ -0,0 +1,17 @@
// 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;
namespace osu.Game.Database
{
public interface IPresentImports<TModel>
where TModel : class
{
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<TModel>> PresentImport { set; }
}
}

View File

@ -1,29 +1,24 @@
// 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 Humanizer;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Online.API;
using osu.Game.Overlays.Notifications;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Online.API;
using osu.Game.Overlays.Notifications;
namespace osu.Game.Database namespace osu.Game.Database
{ {
/// <summary> public abstract class ModelDownloader<TModel> : IModelDownloader<TModel>
/// An <see cref="ArchiveModelManager{TModel, TFileModel}"/> that has the ability to download models using an <see cref="IAPIProvider"/> and where TModel : class, IHasPrimaryKey, ISoftDelete, IEquatable<TModel>
/// import them into the store.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class DownloadableArchiveModelManager<TModel, TFileModel> : ArchiveModelManager<TModel, TFileModel>, IModelDownloader<TModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete, IEquatable<TModel>
where TFileModel : class, INamedFileInfo, new()
{ {
public Action<Notification> PostNotification { protected get; set; }
public IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> DownloadBegan => downloadBegan; public IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> DownloadBegan => downloadBegan;
private readonly Bindable<WeakReference<ArchiveDownloadRequest<TModel>>> downloadBegan = new Bindable<WeakReference<ArchiveDownloadRequest<TModel>>>(); private readonly Bindable<WeakReference<ArchiveDownloadRequest<TModel>>> downloadBegan = new Bindable<WeakReference<ArchiveDownloadRequest<TModel>>>();
@ -32,18 +27,15 @@ namespace osu.Game.Database
private readonly Bindable<WeakReference<ArchiveDownloadRequest<TModel>>> downloadFailed = new Bindable<WeakReference<ArchiveDownloadRequest<TModel>>>(); private readonly Bindable<WeakReference<ArchiveDownloadRequest<TModel>>> downloadFailed = new Bindable<WeakReference<ArchiveDownloadRequest<TModel>>>();
private readonly IModelManager<TModel> modelManager;
private readonly IAPIProvider api; private readonly IAPIProvider api;
private readonly List<ArchiveDownloadRequest<TModel>> currentDownloads = new List<ArchiveDownloadRequest<TModel>>(); private readonly List<ArchiveDownloadRequest<TModel>> currentDownloads = new List<ArchiveDownloadRequest<TModel>>();
private readonly MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore; protected ModelDownloader(IModelManager<TModel> modelManager, IAPIProvider api, IIpcHost importHost = null)
protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes<TModel, TFileModel> modelStore,
IIpcHost importHost = null)
: base(storage, contextFactory, modelStore, importHost)
{ {
this.modelManager = modelManager;
this.api = api; this.api = api;
this.modelStore = modelStore;
} }
/// <summary> /// <summary>
@ -76,7 +68,7 @@ namespace osu.Game.Database
Task.Factory.StartNew(async () => Task.Factory.StartNew(async () =>
{ {
// This gets scheduled back to the update thread, but we want the import to run in the background. // This gets scheduled back to the update thread, but we want the import to run in the background.
var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false); var imported = await modelManager.Import(notification, new ImportTask(filename)).ConfigureAwait(false);
// for now a failed import will be marked as a failed download for simplicity. // for now a failed import will be marked as a failed download for simplicity.
if (!imported.Any()) if (!imported.Any())
@ -111,21 +103,10 @@ namespace osu.Game.Database
notification.State = ProgressNotificationState.Cancelled; notification.State = ProgressNotificationState.Cancelled;
if (!(error is OperationCanceledException)) if (!(error is OperationCanceledException))
Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!"); Logger.Error(error, $"{modelManager.HumanisedModelName.Titleize()} download failed!");
} }
} }
public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending));
/// <summary>
/// Performs implementation specific comparisons to determine whether a given model is present in the local store.
/// </summary>
/// <param name="model">The <typeparamref name="TModel"/> whose existence needs to be checked.</param>
/// <param name="items">The usable items present in the store.</param>
/// <returns>Whether the <typeparamref name="TModel"/> exists.</returns>
protected virtual bool CheckLocalAvailability(TModel model, IQueryable<TModel> items)
=> model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any());
public ArchiveDownloadRequest<TModel> GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model)); public ArchiveDownloadRequest<TModel> GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model));
private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null; private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null;

View File

@ -16,7 +16,7 @@ namespace osu.Game.Online
/// </summary> /// </summary>
public abstract class DownloadTrackingComposite<TModel, TModelManager> : CompositeDrawable public abstract class DownloadTrackingComposite<TModel, TModelManager> : CompositeDrawable
where TModel : class, IEquatable<TModel> where TModel : class, IEquatable<TModel>
where TModelManager : class, IModelDownloader<TModel> where TModelManager : class, IModelDownloader<TModel>, IModelManager<TModel>
{ {
protected readonly Bindable<TModel> Model = new Bindable<TModel>(); protected readonly Bindable<TModel> Model = new Bindable<TModel>();
@ -35,7 +35,7 @@ namespace osu.Game.Online
Model.Value = model; Model.Value = model;
} }
private IBindable<WeakReference<TModel>> managedUpdated; private IBindable<WeakReference<TModel>> managerUpdated;
private IBindable<WeakReference<TModel>> managerRemoved; private IBindable<WeakReference<TModel>> managerRemoved;
private IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> managerDownloadBegan; private IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> managerDownloadBegan;
private IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> managerDownloadFailed; private IBindable<WeakReference<ArchiveDownloadRequest<TModel>>> managerDownloadFailed;
@ -60,8 +60,8 @@ namespace osu.Game.Online
managerDownloadBegan.BindValueChanged(downloadBegan); managerDownloadBegan.BindValueChanged(downloadBegan);
managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy(); managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy();
managerDownloadFailed.BindValueChanged(downloadFailed); managerDownloadFailed.BindValueChanged(downloadFailed);
managedUpdated = Manager.ItemUpdated.GetBoundCopy(); managerUpdated = Manager.ItemUpdated.GetBoundCopy();
managedUpdated.BindValueChanged(itemUpdated); managerUpdated.BindValueChanged(itemUpdated);
managerRemoved = Manager.ItemRemoved.GetBoundCopy(); managerRemoved = Manager.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(itemRemoved); managerRemoved.BindValueChanged(itemRemoved);
} }
@ -77,7 +77,7 @@ namespace osu.Game.Online
/// <summary> /// <summary>
/// Whether the given model is available in the database. /// Whether the given model is available in the database.
/// By default, this calls <see cref="IModelDownloader{TModel}.IsAvailableLocally"/>, /// By default, this calls <see cref="IModelManager{TModel}.IsAvailableLocally"/>,
/// but can be overriden to add additional checks for verifying the model in database. /// but can be overriden to add additional checks for verifying the model in database.
/// </summary> /// </summary>
protected virtual bool IsModelAvailableLocally() => Manager?.IsAvailableLocally(Model.Value) == true; protected virtual bool IsModelAvailableLocally() => Manager?.IsAvailableLocally(Model.Value) == true;

View File

@ -9,102 +9,48 @@ using System.Linq.Expressions;
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.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
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.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
public class ScoreManager : DownloadableArchiveModelManager<ScoreInfo, ScoreFileInfo> public class ScoreManager : IModelManager<ScoreInfo>, IModelFileManager<ScoreInfo, ScoreFileInfo>, IModelDownloader<ScoreInfo>, ICanAcceptFiles, IPresentImports<ScoreInfo>
{ {
public override IEnumerable<string> HandledExtensions => new[] { ".osr" };
protected override string[] HashableFileTypes => new[] { ".osr" };
protected override string ImportFromStablePath => Path.Combine("Data", "r");
private readonly RulesetStore rulesets;
private readonly Func<BeatmapManager> beatmaps;
private readonly Scheduler scheduler; private readonly Scheduler scheduler;
[CanBeNull]
private readonly Func<BeatmapDifficultyCache> difficulties; private readonly Func<BeatmapDifficultyCache> difficulties;
[CanBeNull]
private readonly OsuConfigManager configManager; private readonly OsuConfigManager configManager;
private readonly ScoreModelManager scoreModelManager;
private readonly ScoreModelDownloader scoreModelDownloader;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler, public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler,
IIpcHost importHost = null, Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null) IIpcHost importHost = null, Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
: base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost)
{ {
this.rulesets = rulesets;
this.beatmaps = beatmaps;
this.scheduler = scheduler; this.scheduler = scheduler;
this.difficulties = difficulties; this.difficulties = difficulties;
this.configManager = configManager; this.configManager = configManager;
scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory, importHost);
scoreModelDownloader = new ScoreModelDownloader(scoreModelManager, api, importHost);
} }
protected override ScoreInfo CreateModel(ArchiveReader archive) public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score);
{
if (archive == null)
return null;
using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) public List<ScoreInfo> GetAllUsableScores() => scoreModelManager.GetAllUsableScores();
{
try
{
return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo;
}
catch (LegacyScoreDecoder.BeatmapNotFoundException e)
{
Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error);
return null;
}
}
}
protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) public IEnumerable<ScoreInfo> QueryScores(Expression<Func<ScoreInfo, bool>> query) => scoreModelManager.QueryScores(query);
=> Task.CompletedTask;
public override void ExportModelTo(ScoreInfo model, Stream outputStream) public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query) => scoreModelManager.Query(query);
{
var file = model.Files.SingleOrDefault();
if (file == null)
return;
using (var inputStream = Files.Storage.GetStream(file.FileInfo.StoragePath))
inputStream.CopyTo(outputStream);
}
protected override IEnumerable<string> GetStableImportPaths(Storage storage)
=> storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
.Select(path => storage.GetFullPath(path));
public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store);
public List<ScoreInfo> GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
public IEnumerable<ScoreInfo> QueryScores(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query);
public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query);
protected override ArchiveDownloadRequest<ScoreInfo> CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score);
protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable<ScoreInfo> items)
=> base.CheckLocalAvailability(model, items)
|| (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID));
/// <summary> /// <summary>
/// Orders an array of <see cref="ScoreInfo"/>s by total score. /// Orders an array of <see cref="ScoreInfo"/>s by total score.
@ -281,5 +227,149 @@ namespace osu.Game.Scoring
this.totalScore.BindValueChanged(v => Value = v.NewValue.ToString("N0"), true); this.totalScore.BindValueChanged(v => Value = v.NewValue.ToString("N0"), true);
} }
} }
#region Implementation of IPostNotifications
public Action<Notification> PostNotification
{
set
{
scoreModelManager.PostNotification = value;
scoreModelDownloader.PostNotification = value;
}
}
#endregion
#region Implementation of IModelManager<ScoreInfo>
public IBindable<WeakReference<ScoreInfo>> ItemUpdated => scoreModelManager.ItemUpdated;
public IBindable<WeakReference<ScoreInfo>> ItemRemoved => scoreModelManager.ItemRemoved;
public Task ImportFromStableAsync(StableStorage stableStorage)
{
return scoreModelManager.ImportFromStableAsync(stableStorage);
}
public void Export(ScoreInfo item)
{
scoreModelManager.Export(item);
}
public void ExportModelTo(ScoreInfo model, Stream outputStream)
{
scoreModelManager.ExportModelTo(model, outputStream);
}
public void Update(ScoreInfo item)
{
scoreModelManager.Update(item);
}
public bool Delete(ScoreInfo item)
{
return scoreModelManager.Delete(item);
}
public void Delete(List<ScoreInfo> items, bool silent = false)
{
scoreModelManager.Delete(items, silent);
}
public void Undelete(List<ScoreInfo> items, bool silent = false)
{
scoreModelManager.Undelete(items, silent);
}
public void Undelete(ScoreInfo item)
{
scoreModelManager.Undelete(item);
}
public Task Import(params string[] paths)
{
return scoreModelManager.Import(paths);
}
public Task Import(params ImportTask[] tasks)
{
return scoreModelManager.Import(tasks);
}
public IEnumerable<string> HandledExtensions => scoreModelManager.HandledExtensions;
public Task<IEnumerable<ScoreInfo>> Import(ProgressNotification notification, params ImportTask[] tasks)
{
return scoreModelManager.Import(notification, tasks);
}
public Task<ScoreInfo> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return scoreModelManager.Import(task, lowPriority, cancellationToken);
}
public Task<ScoreInfo> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return scoreModelManager.Import(archive, lowPriority, cancellationToken);
}
public Task<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return scoreModelManager.Import(item, archive, lowPriority, cancellationToken);
}
public bool IsAvailableLocally(ScoreInfo model)
{
return scoreModelManager.IsAvailableLocally(model);
}
#endregion
#region Implementation of IModelFileManager<in ScoreInfo,in ScoreFileInfo>
public void ReplaceFile(ScoreInfo model, ScoreFileInfo file, Stream contents, string filename = null)
{
scoreModelManager.ReplaceFile(model, file, contents, filename);
}
public void DeleteFile(ScoreInfo model, ScoreFileInfo file)
{
scoreModelManager.DeleteFile(model, file);
}
public void AddFile(ScoreInfo model, Stream contents, string filename)
{
scoreModelManager.AddFile(model, contents, filename);
}
#endregion
#region Implementation of IModelDownloader<ScoreInfo>
public IBindable<WeakReference<ArchiveDownloadRequest<ScoreInfo>>> DownloadBegan => scoreModelDownloader.DownloadBegan;
public IBindable<WeakReference<ArchiveDownloadRequest<ScoreInfo>>> DownloadFailed => scoreModelDownloader.DownloadFailed;
public bool Download(ScoreInfo model, bool minimiseDownloadSize)
{
return scoreModelDownloader.Download(model, minimiseDownloadSize);
}
public ArchiveDownloadRequest<ScoreInfo> GetExistingDownload(ScoreInfo model)
{
return scoreModelDownloader.GetExistingDownload(model);
}
#endregion
#region Implementation of IPresentImports<ScoreInfo>
public Action<IEnumerable<ScoreInfo>> PresentImport
{
set => scoreModelManager.PresentImport = value;
}
#endregion
} }
} }

View File

@ -0,0 +1,20 @@
// 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 osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
namespace osu.Game.Scoring
{
public class ScoreModelDownloader : ModelDownloader<ScoreInfo>
{
public ScoreModelDownloader(ScoreModelManager scoreManager, IAPIProvider api, IIpcHost importHost = null)
: base(scoreManager, api, importHost)
{
}
protected override ArchiveDownloadRequest<ScoreInfo> CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score);
}
}

View File

@ -0,0 +1,88 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Rulesets;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Scoring
{
public class ScoreModelManager : ArchiveModelManager<ScoreInfo, ScoreFileInfo>
{
public override IEnumerable<string> HandledExtensions => new[] { ".osr" };
protected override string[] HashableFileTypes => new[] { ".osr" };
protected override string ImportFromStablePath => Path.Combine("Data", "r");
private readonly RulesetStore rulesets;
private readonly Func<BeatmapManager> beatmaps;
public ScoreModelManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IDatabaseContextFactory contextFactory, IIpcHost importHost = null)
: base(storage, contextFactory, new ScoreStore(contextFactory, storage), importHost)
{
this.rulesets = rulesets;
this.beatmaps = beatmaps;
}
protected override ScoreInfo CreateModel(ArchiveReader archive)
{
if (archive == null)
return null;
using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase))))
{
try
{
return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo;
}
catch (LegacyScoreDecoder.BeatmapNotFoundException e)
{
Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error);
return null;
}
}
}
public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store);
public List<ScoreInfo> GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
public IEnumerable<ScoreInfo> QueryScores(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query);
public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query);
protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable<ScoreInfo> items)
=> base.CheckLocalAvailability(model, items)
|| (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID));
public override void ExportModelTo(ScoreInfo model, Stream outputStream)
{
var file = model.Files.SingleOrDefault();
if (file == null)
return;
using (var inputStream = Files.Storage.GetStream(file.FileInfo.StoragePath))
inputStream.CopyTo(outputStream);
}
protected override IEnumerable<string> GetStableImportPaths(Storage storage)
=> storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
.Select(path => storage.GetFullPath(path));
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Screens.Ranking
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuGame game, ScoreManager scores) private void load(OsuGame game, ScoreModelDownloader scores)
{ {
InternalChild = shakeContainer = new ShakeContainer InternalChild = shakeContainer = new ShakeContainer
{ {

View File

@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual
internal class TestBeatmapModelManager : BeatmapModelManager internal class TestBeatmapModelManager : BeatmapModelManager
{ {
public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost)
: base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) : base(storage, databaseContextFactory, rulesetStore, gameHost)
{ {
} }