mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 09:02:58 +08:00
Merge pull request #2070 from peppy/abstract-model-managers
Abstract away model-filemodel and add mutable stores
This commit is contained in:
commit
999d0f99a6
@ -111,16 +111,11 @@ namespace osu.Desktop
|
||||
{
|
||||
var filePaths = new [] { e.FileName };
|
||||
|
||||
if (filePaths.All(f => Path.GetExtension(f) == @".osz"))
|
||||
Task.Factory.StartNew(() => BeatmapManager.Import(filePaths), TaskCreationOptions.LongRunning);
|
||||
else if (filePaths.All(f => Path.GetExtension(f) == @".osr"))
|
||||
Task.Run(() =>
|
||||
{
|
||||
var score = ScoreStore.ReadReplayFile(filePaths.First());
|
||||
Schedule(() => LoadScore(score));
|
||||
});
|
||||
}
|
||||
var firstExtension = Path.GetExtension(filePaths.First());
|
||||
|
||||
private static readonly string[] allowed_extensions = { @".osz", @".osr" };
|
||||
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
|
||||
|
||||
Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Desktop
|
||||
{
|
||||
if (!host.IsPrimaryInstance)
|
||||
{
|
||||
var importer = new BeatmapIPCChannel(host);
|
||||
var importer = new ArchiveImportIPCChannel(host);
|
||||
// Restore the cwd so relative paths given at the command line work correctly
|
||||
Directory.SetCurrentDirectory(cwd);
|
||||
foreach (var file in args)
|
||||
|
@ -165,7 +165,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
var temp = prepareTempCopy(osz_path);
|
||||
Assert.IsTrue(File.Exists(temp));
|
||||
|
||||
var importer = new BeatmapIPCChannel(client);
|
||||
var importer = new ArchiveImportIPCChannel(client);
|
||||
if (!importer.ImportAsync(temp).Wait(10000))
|
||||
Assert.Fail(@"IPC took too long to send");
|
||||
|
||||
@ -209,7 +209,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
Assert.IsTrue(File.Exists(temp));
|
||||
|
||||
var imported = osu.Dependencies.Get<BeatmapManager>().Import(temp);
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
manager.Import(temp);
|
||||
|
||||
var imported = manager.GetAllUsableBeatmapSets();
|
||||
|
||||
ensureLoaded(osu);
|
||||
|
||||
|
@ -5,9 +5,9 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.IO;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO.Archives;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
@ -19,7 +19,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz"))
|
||||
{
|
||||
var reader = new OszArchiveReader(osz);
|
||||
var reader = new ZipArchiveReader(osz);
|
||||
string[] expected =
|
||||
{
|
||||
"Soleily - Renatus (Deif) [Platter].osu",
|
||||
@ -46,7 +46,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz"))
|
||||
{
|
||||
var reader = new OszArchiveReader(osz);
|
||||
var reader = new ZipArchiveReader(osz);
|
||||
|
||||
BeatmapMetadata meta;
|
||||
using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
|
||||
@ -71,7 +71,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz"))
|
||||
{
|
||||
var reader = new OszArchiveReader(osz);
|
||||
var reader = new ZipArchiveReader(osz);
|
||||
using (var stream = new StreamReader(
|
||||
reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
|
||||
{
|
||||
|
@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
if (deleteMaps)
|
||||
{
|
||||
manager.DeleteAll();
|
||||
manager.Delete(manager.GetAllUsableBeatmapSets());
|
||||
game.Beatmap.SetDefault();
|
||||
}
|
||||
|
||||
|
@ -7,17 +7,14 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Ionic.Zip;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Beatmaps.IO;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -28,23 +25,13 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
|
||||
/// </summary>
|
||||
public partial class BeatmapManager
|
||||
public partial class BeatmapManager : ArchiveModelManager<BeatmapSetInfo, BeatmapSetFileInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when a new <see cref="BeatmapSetInfo"/> becomes available in the database.
|
||||
/// </summary>
|
||||
public event Action<BeatmapSetInfo> BeatmapSetAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been hidden.
|
||||
/// </summary>
|
||||
public event Action<BeatmapInfo> BeatmapHidden;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a <see cref="BeatmapSetInfo"/> is removed from the database.
|
||||
/// </summary>
|
||||
public event Action<BeatmapSetInfo> BeatmapSetRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been restored.
|
||||
/// </summary>
|
||||
@ -60,9 +47,7 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public WorkingBeatmap DefaultBeatmap { private get; set; }
|
||||
|
||||
private readonly IDatabaseContextFactory contextFactory;
|
||||
|
||||
private readonly FileStore files;
|
||||
public override string[] HandledExtensions => new[] { ".osz" };
|
||||
|
||||
private readonly RulesetStore rulesets;
|
||||
|
||||
@ -72,150 +57,56 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
|
||||
private BeatmapIPCChannel ipc;
|
||||
|
||||
/// <summary>
|
||||
/// Set an endpoint for notifications to be posted to.
|
||||
/// </summary>
|
||||
public Action<Notification> PostNotification { private get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set a storage with access to an osu-stable install for import purposes.
|
||||
/// </summary>
|
||||
public Func<Storage> GetStableStorage { private get; set; }
|
||||
|
||||
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
|
||||
: base(storage, contextFactory, new BeatmapStore(contextFactory), importHost)
|
||||
{
|
||||
this.contextFactory = contextFactory;
|
||||
|
||||
beatmaps = new BeatmapStore(contextFactory);
|
||||
|
||||
beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s);
|
||||
beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s);
|
||||
beatmaps = (BeatmapStore)ModelStore;
|
||||
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
|
||||
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
|
||||
|
||||
files = new FileStore(contextFactory, storage);
|
||||
|
||||
this.rulesets = rulesets;
|
||||
this.api = api;
|
||||
|
||||
if (importHost != null)
|
||||
ipc = new BeatmapIPCChannel(importHost, this);
|
||||
|
||||
beatmaps.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import one or more <see cref="BeatmapSetInfo"/> from filesystem <paramref name="paths"/>.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
/// <param name="paths">One or more beatmap locations on disk.</param>
|
||||
public List<BeatmapSetInfo> Import(params string[] paths)
|
||||
protected override void Populate(BeatmapSetInfo model, ArchiveReader archive)
|
||||
{
|
||||
var notification = new ProgressNotification
|
||||
model.Beatmaps = createBeatmapDifficulties(archive);
|
||||
|
||||
// remove metadata from difficulties where it matches the set
|
||||
foreach (BeatmapInfo b in model.Beatmaps)
|
||||
if (model.Metadata.Equals(b.Metadata))
|
||||
b.Metadata = null;
|
||||
}
|
||||
|
||||
protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model)
|
||||
{
|
||||
// check if this beatmap has already been imported and exit early if so
|
||||
var existingHashMatch = beatmaps.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
|
||||
if (existingHashMatch != null)
|
||||
{
|
||||
Text = "Beatmap import is initialising...",
|
||||
CompletionText = "Import successful!",
|
||||
Progress = 0,
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
Undelete(existingHashMatch);
|
||||
return existingHashMatch;
|
||||
}
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
List<BeatmapSetInfo> imported = new List<BeatmapSetInfo>();
|
||||
|
||||
int i = 0;
|
||||
foreach (string path in paths)
|
||||
// check if a set already exists with the same online id
|
||||
if (model.OnlineBeatmapSetID != null)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return imported;
|
||||
|
||||
try
|
||||
var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID);
|
||||
if (existingOnlineId != null)
|
||||
{
|
||||
notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}";
|
||||
using (ArchiveReader reader = getReaderFrom(path))
|
||||
imported.Add(Import(reader));
|
||||
|
||||
notification.Progress = (float)++i / paths.Length;
|
||||
|
||||
// We may or may not want to delete the file depending on where it is stored.
|
||||
// e.g. reconstructing/repairing database with beatmaps from default storage.
|
||||
// Also, not always a single file, i.e. for LegacyFilesystemReader
|
||||
// TODO: Add a check to prevent files from storage to be deleted.
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e = e.InnerException ?? e;
|
||||
Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})");
|
||||
Delete(existingOnlineId);
|
||||
beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID);
|
||||
}
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
return imported;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import a beatmap from an <see cref="ArchiveReader"/>.
|
||||
/// </summary>
|
||||
/// <param name="archive">The beatmap to be imported.</param>
|
||||
public BeatmapSetInfo Import(ArchiveReader archive)
|
||||
{
|
||||
using (contextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
|
||||
{
|
||||
// create a new set info (don't yet add to database)
|
||||
var beatmapSet = createBeatmapSetInfo(archive);
|
||||
|
||||
// check if this beatmap has already been imported and exit early if so
|
||||
var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == beatmapSet.Hash);
|
||||
if (existingHashMatch != null)
|
||||
{
|
||||
Undelete(existingHashMatch);
|
||||
return existingHashMatch;
|
||||
}
|
||||
|
||||
// check if a set already exists with the same online id
|
||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||
{
|
||||
var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
|
||||
if (existingOnlineId != null)
|
||||
{
|
||||
Delete(existingOnlineId);
|
||||
beatmaps.Cleanup(s => s.ID == existingOnlineId.ID);
|
||||
}
|
||||
}
|
||||
|
||||
beatmapSet.Files = createFileInfos(archive, files);
|
||||
beatmapSet.Beatmaps = createBeatmapDifficulties(archive);
|
||||
|
||||
// remove metadata from difficulties where it matches the set
|
||||
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
|
||||
if (beatmapSet.Metadata.Equals(b.Metadata))
|
||||
b.Metadata = null;
|
||||
|
||||
// import to beatmap store
|
||||
Import(beatmapSet);
|
||||
return beatmapSet;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import a beatmap from a <see cref="BeatmapSetInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">The beatmap to be imported.</param>
|
||||
public void Import(BeatmapSetInfo beatmapSet) => beatmaps.Add(beatmapSet);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a beatmap.
|
||||
/// This will post notifications tracking progress.
|
||||
@ -260,7 +151,7 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
// This gets scheduled back to the update thread, but we want the import to run in the background.
|
||||
using (var stream = new MemoryStream(data))
|
||||
using (var archive = new OszArchiveReader(stream))
|
||||
using (var archive = new ZipArchiveReader(stream, beatmapSetInfo.ToString()))
|
||||
Import(archive);
|
||||
|
||||
downloadNotification.State = ProgressNotificationState.Completed;
|
||||
@ -300,95 +191,6 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>The <see cref="DownloadBeatmapSetRequest"/> object if it exists, or null.</returns>
|
||||
public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID);
|
||||
|
||||
/// <summary>
|
||||
/// Update a BeatmapSetInfo with all changes. TODO: This only supports very basic updates currently.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">The beatmap set to update.</param>
|
||||
public void Update(BeatmapSetInfo beatmap) => beatmaps.Update(beatmap);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a beatmap from the manager.
|
||||
/// Is a no-op for already deleted beatmaps.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">The beatmap set to delete.</param>
|
||||
public void Delete(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
using (var usage = contextFactory.GetForWrite())
|
||||
{
|
||||
var context = usage.Context;
|
||||
|
||||
context.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
|
||||
// re-fetch the beatmap set on the import context.
|
||||
beatmapSet = context.BeatmapSetInfo.Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == beatmapSet.ID);
|
||||
|
||||
if (beatmaps.Delete(beatmapSet))
|
||||
{
|
||||
if (!beatmapSet.Protected)
|
||||
files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
|
||||
}
|
||||
|
||||
context.ChangeTracker.AutoDetectChangesEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore all beatmaps that were previously deleted.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void UndeleteAll()
|
||||
{
|
||||
var deleteMaps = QueryBeatmapSets(bs => bs.DeletePending).ToList();
|
||||
|
||||
if (!deleteMaps.Any()) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
CompletionText = "Restored all deleted beatmaps!",
|
||||
Progress = 0,
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach (var bs in deleteMaps)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
notification.Text = $"Restoring ({i} of {deleteMaps.Count})";
|
||||
notification.Progress = (float)++i / deleteMaps.Count;
|
||||
Undelete(bs);
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore a beatmap that was previously deleted. Is a no-op if the beatmap is not in a deleted state, or has its protected flag set.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">The beatmap to restore</param>
|
||||
public void Undelete(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
if (beatmapSet.Protected)
|
||||
return;
|
||||
|
||||
using (var usage = contextFactory.GetForWrite())
|
||||
{
|
||||
usage.Context.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
|
||||
if (!beatmaps.Undelete(beatmapSet)) return;
|
||||
|
||||
if (!beatmapSet.Protected)
|
||||
files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
|
||||
|
||||
usage.Context.ChangeTracker.AutoDetectChangesEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a beatmap difficulty.
|
||||
/// </summary>
|
||||
@ -415,7 +217,7 @@ namespace osu.Game.Beatmaps
|
||||
if (beatmapInfo.Metadata == null)
|
||||
beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata;
|
||||
|
||||
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(files.Store, beatmapInfo);
|
||||
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, beatmapInfo);
|
||||
|
||||
previous?.TransferTo(working);
|
||||
|
||||
@ -427,27 +229,20 @@ namespace osu.Game.Beatmaps
|
||||
/// </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.BeatmapSets.AsNoTracking().FirstOrDefault(query);
|
||||
|
||||
/// <summary>
|
||||
/// Refresh an existing instance of a <see cref="BeatmapSetInfo"/> from the store.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">A stale instance.</param>
|
||||
/// <returns>A fresh instance.</returns>
|
||||
public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID);
|
||||
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
|
||||
|
||||
/// <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() => beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList();
|
||||
public List<BeatmapSetInfo> GetAllUsableBeatmapSets() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>Results from the provided query.</returns>
|
||||
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.BeatmapSets.AsNoTracking().Where(query);
|
||||
public IEnumerable<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().Where(query);
|
||||
|
||||
/// <summary>
|
||||
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
|
||||
@ -484,54 +279,6 @@ namespace osu.Game.Beatmaps
|
||||
await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete all beatmaps.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void DeleteAll()
|
||||
{
|
||||
var maps = GetAllUsableBeatmapSets();
|
||||
|
||||
if (maps.Count == 0) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
Progress = 0,
|
||||
CompletionText = "Deleted all beatmaps!",
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach (var b in maps)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
notification.Text = $"Deleting ({i} of {maps.Count})";
|
||||
notification.Progress = (float)++i / maps.Count;
|
||||
Delete(b);
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
|
||||
/// </summary>
|
||||
/// <param name="path">A file or folder path resolving the beatmap content.</param>
|
||||
/// <returns>A reader giving access to the beatmap's content.</returns>
|
||||
private ArchiveReader getReaderFrom(string path)
|
||||
{
|
||||
if (ZipFile.IsZipFile(path))
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
return new OszArchiveReader(files.Storage.GetStream(path));
|
||||
return new LegacyFilesystemReader(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
|
||||
/// </summary>
|
||||
@ -546,10 +293,7 @@ namespace osu.Game.Beatmaps
|
||||
return hashable.ComputeSHA2Hash();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="BeatmapSetInfo"/> from a provided archive.
|
||||
/// </summary>
|
||||
private BeatmapSetInfo createBeatmapSetInfo(ArchiveReader reader)
|
||||
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"));
|
||||
@ -568,25 +312,6 @@ namespace osu.Game.Beatmaps
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create all required <see cref="FileInfo"/>s for the provided archive, adding them to the global file store.
|
||||
/// </summary>
|
||||
private List<BeatmapSetFileInfo> createFileInfos(ArchiveReader reader, FileStore files)
|
||||
{
|
||||
List<BeatmapSetFileInfo> fileInfos = new List<BeatmapSetFileInfo>();
|
||||
|
||||
// import files to manager
|
||||
foreach (string file in reader.Filenames)
|
||||
using (Stream s = reader.GetStream(file))
|
||||
fileInfos.Add(new BeatmapSetFileInfo
|
||||
{
|
||||
Filename = file,
|
||||
FileInfo = files.Add(s)
|
||||
});
|
||||
|
||||
return fileInfos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
|
||||
/// </summary>
|
||||
|
@ -3,11 +3,12 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public class BeatmapSetFileInfo
|
||||
public class BeatmapSetFileInfo : INamedFileInfo
|
||||
{
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int ID { get; set; }
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public class BeatmapSetInfo : IHasPrimaryKey
|
||||
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete
|
||||
{
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int ID { get; set; }
|
||||
|
@ -2,8 +2,8 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Game.Database;
|
||||
|
||||
@ -12,11 +12,8 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing
|
||||
/// </summary>
|
||||
public class BeatmapStore : DatabaseBackedStore
|
||||
public class BeatmapStore : MutableDatabaseBackedStore<BeatmapSetInfo>
|
||||
{
|
||||
public event Action<BeatmapSetInfo> BeatmapSetAdded;
|
||||
public event Action<BeatmapSetInfo> BeatmapSetRemoved;
|
||||
|
||||
public event Action<BeatmapInfo> BeatmapHidden;
|
||||
public event Action<BeatmapInfo> BeatmapRestored;
|
||||
|
||||
@ -25,88 +22,6 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a <see cref="BeatmapSetInfo"/> to the database.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">The beatmap to add.</param>
|
||||
public void Add(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
var context = usage.Context;
|
||||
|
||||
foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null))
|
||||
{
|
||||
// If we detect a new metadata object it'll be attached to the current context so it can be reused
|
||||
// to prevent duplicate entries when persisting. To accomplish this we look in the cache (.Local)
|
||||
// of the corresponding table (.Set<BeatmapMetadata>()) for matching entries to our criteria.
|
||||
var contextMetadata = context.Set<BeatmapMetadata>().Local.SingleOrDefault(e => e.Equals(beatmap.Metadata));
|
||||
if (contextMetadata != null)
|
||||
beatmap.Metadata = contextMetadata;
|
||||
else
|
||||
context.BeatmapMetadata.Attach(beatmap.Metadata);
|
||||
}
|
||||
|
||||
context.BeatmapSetInfo.Attach(beatmapSet);
|
||||
|
||||
BeatmapSetAdded?.Invoke(beatmapSet);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a <see cref="BeatmapSetInfo"/> in the database. TODO: This only supports very basic updates currently.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">The beatmap to update.</param>
|
||||
public void Update(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
BeatmapSetRemoved?.Invoke(beatmapSet);
|
||||
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
usage.Context.BeatmapSetInfo.Update(beatmapSet);
|
||||
|
||||
BeatmapSetAdded?.Invoke(beatmapSet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a <see cref="BeatmapSetInfo"/> from the database.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">The beatmap to delete.</param>
|
||||
/// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns>
|
||||
public bool Delete(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
Refresh(ref beatmapSet, BeatmapSets);
|
||||
|
||||
if (beatmapSet.DeletePending) return false;
|
||||
|
||||
beatmapSet.DeletePending = true;
|
||||
}
|
||||
|
||||
BeatmapSetRemoved?.Invoke(beatmapSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore a previously deleted <see cref="BeatmapSetInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmapSet">The beatmap to restore.</param>
|
||||
/// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns>
|
||||
public bool Undelete(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
Refresh(ref beatmapSet, BeatmapSets);
|
||||
|
||||
if (!beatmapSet.DeletePending) return false;
|
||||
|
||||
beatmapSet.DeletePending = false;
|
||||
}
|
||||
|
||||
BeatmapSetAdded?.Invoke(beatmapSet);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide a <see cref="BeatmapInfo"/> in the database.
|
||||
/// </summary>
|
||||
@ -119,12 +34,10 @@ namespace osu.Game.Beatmaps
|
||||
Refresh(ref beatmap, Beatmaps);
|
||||
|
||||
if (beatmap.Hidden) return false;
|
||||
|
||||
beatmap.Hidden = true;
|
||||
|
||||
BeatmapHidden?.Invoke(beatmap);
|
||||
}
|
||||
|
||||
BeatmapHidden?.Invoke(beatmap);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -140,7 +53,6 @@ namespace osu.Game.Beatmaps
|
||||
Refresh(ref beatmap, Beatmaps);
|
||||
|
||||
if (!beatmap.Hidden) return false;
|
||||
|
||||
beatmap.Hidden = false;
|
||||
}
|
||||
|
||||
@ -148,46 +60,38 @@ namespace osu.Game.Beatmaps
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Cleanup() => Cleanup(_ => true);
|
||||
protected override IQueryable<BeatmapSetInfo> AddIncludesForDeletion(IQueryable<BeatmapSetInfo> query) =>
|
||||
base.AddIncludesForDeletion(query)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
|
||||
.Include(s => s.Metadata);
|
||||
|
||||
public void Cleanup(Expression<Func<BeatmapSetInfo, bool>> query)
|
||||
protected override IQueryable<BeatmapSetInfo> AddIncludesForConsumption(IQueryable<BeatmapSetInfo> query) =>
|
||||
base.AddIncludesForConsumption(query)
|
||||
.Include(s => s.Metadata)
|
||||
.Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
|
||||
.Include(s => s.Files).ThenInclude(f => f.FileInfo);
|
||||
|
||||
protected override void Purge(List<BeatmapSetInfo> items, OsuDbContext context)
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
var context = usage.Context;
|
||||
// metadata is M-N so we can't rely on cascades
|
||||
context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata));
|
||||
context.BeatmapMetadata.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null)));
|
||||
|
||||
var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected)
|
||||
.Where(query)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
|
||||
.Include(s => s.Metadata).ToList();
|
||||
// todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly.
|
||||
context.BeatmapDifficulty.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty)));
|
||||
|
||||
if (!purgeable.Any()) return;
|
||||
|
||||
// metadata is M-N so we can't rely on cascades
|
||||
context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata));
|
||||
context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null)));
|
||||
|
||||
// todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly.
|
||||
context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty)));
|
||||
|
||||
// cascades down to beatmaps.
|
||||
context.BeatmapSetInfo.RemoveRange(purgeable);
|
||||
}
|
||||
base.Purge(items, context);
|
||||
}
|
||||
|
||||
public IQueryable<BeatmapSetInfo> BeatmapSets => ContextFactory.Get().BeatmapSetInfo
|
||||
.Include(s => s.Metadata)
|
||||
.Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
|
||||
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
|
||||
.Include(s => s.Files).ThenInclude(f => f.FileInfo);
|
||||
|
||||
public IQueryable<BeatmapInfo> Beatmaps => ContextFactory.Get().BeatmapInfo
|
||||
.Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
|
||||
.Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo)
|
||||
.Include(b => b.Metadata)
|
||||
.Include(b => b.Ruleset)
|
||||
.Include(b => b.BaseDifficulty);
|
||||
public IQueryable<BeatmapInfo> Beatmaps =>
|
||||
ContextFactory.Get().BeatmapInfo
|
||||
.Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
|
||||
.Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo)
|
||||
.Include(b => b.Metadata)
|
||||
.Include(b => b.Ruleset)
|
||||
.Include(b => b.BaseDifficulty);
|
||||
}
|
||||
}
|
||||
|
337
osu.Game/Database/ArchiveModelManager.cs
Normal file
337
osu.Game/Database/ArchiveModelManager.cs
Normal file
@ -0,0 +1,337 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Ionic.Zip;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using FileInfo = osu.Game.IO.FileInfo;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates a model store class to give it import functionality.
|
||||
/// Adds cross-functionality with <see cref="FileStore"/> to give access to the central file store for the provided model.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The model type.</typeparam>
|
||||
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
|
||||
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles
|
||||
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
|
||||
where TFileModel : INamedFileInfo, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Set an endpoint for notifications to be posted to.
|
||||
/// </summary>
|
||||
public Action<Notification> PostNotification { protected get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a new <see cref="TModel"/> becomes available in the database.
|
||||
/// </summary>
|
||||
public event Action<TModel> ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a <see cref="TModel"/> is removed from the database.
|
||||
/// </summary>
|
||||
public event Action<TModel> ItemRemoved;
|
||||
|
||||
public virtual string[] HandledExtensions => new[] { ".zip" };
|
||||
|
||||
protected readonly FileStore Files;
|
||||
|
||||
protected readonly IDatabaseContextFactory ContextFactory;
|
||||
|
||||
protected readonly MutableDatabaseBackedStore<TModel> ModelStore;
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
|
||||
private ArchiveImportIPCChannel ipc;
|
||||
|
||||
protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStore<TModel> modelStore, IIpcHost importHost = null)
|
||||
{
|
||||
ContextFactory = contextFactory;
|
||||
|
||||
ModelStore = modelStore;
|
||||
ModelStore.ItemAdded += s => ItemAdded?.Invoke(s);
|
||||
ModelStore.ItemRemoved += s => ItemRemoved?.Invoke(s);
|
||||
|
||||
Files = new FileStore(contextFactory, storage);
|
||||
|
||||
if (importHost != null)
|
||||
ipc = new ArchiveImportIPCChannel(importHost, this);
|
||||
|
||||
ModelStore.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import one or more <see cref="TModel"/> items from filesystem <paramref name="paths"/>.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
/// <param name="paths">One or more archive locations on disk.</param>
|
||||
public void Import(params string[] paths)
|
||||
{
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
Text = "Import is initialising...",
|
||||
CompletionText = "Import successful!",
|
||||
Progress = 0,
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
List<TModel> imported = new List<TModel>();
|
||||
|
||||
int i = 0;
|
||||
foreach (string path in paths)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}";
|
||||
using (ArchiveReader reader = getReaderFrom(path))
|
||||
imported.Add(Import(reader));
|
||||
|
||||
notification.Progress = (float)++i / paths.Length;
|
||||
|
||||
// We may or may not want to delete the file depending on where it is stored.
|
||||
// e.g. reconstructing/repairing database with items from default storage.
|
||||
// Also, not always a single file, i.e. for LegacyFilesystemReader
|
||||
// TODO: Add a check to prevent files from storage to be deleted.
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e = e.InnerException ?? e;
|
||||
Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})");
|
||||
}
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import an item from an <see cref="ArchiveReader"/>.
|
||||
/// </summary>
|
||||
/// <param name="archive">The archive to be imported.</param>
|
||||
public TModel Import(ArchiveReader archive)
|
||||
{
|
||||
using (ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
|
||||
{
|
||||
// create a new model (don't yet add to database)
|
||||
var item = CreateModel(archive);
|
||||
|
||||
var existing = CheckForExisting(item);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
item.Files = createFileInfos(archive, Files);
|
||||
|
||||
Populate(item, archive);
|
||||
|
||||
// import to store
|
||||
ModelStore.Add(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import an item from a <see cref="TModel"/>.
|
||||
/// </summary>
|
||||
/// <param name="item">The model to be imported.</param>
|
||||
public void Import(TModel item) => ModelStore.Add(item);
|
||||
|
||||
/// <summary>
|
||||
/// Perform an update of the specified item.
|
||||
/// TODO: Support file changes.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to update.</param>
|
||||
public void Update(TModel item) => ModelStore.Update(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>
|
||||
public void Delete(TModel item)
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
var context = usage.Context;
|
||||
|
||||
context.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
|
||||
// re-fetch the model on the import context.
|
||||
var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == item.ID);
|
||||
|
||||
if (foundModel.DeletePending) return;
|
||||
|
||||
if (ModelStore.Delete(foundModel))
|
||||
Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray());
|
||||
|
||||
context.ChangeTracker.AutoDetectChangesEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete multiple items.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void Delete(List<TModel> items)
|
||||
{
|
||||
if (items.Count == 0) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
Progress = 0,
|
||||
CompletionText = "Deleted all beatmaps!",
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
foreach (var b in items)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
notification.Text = $"Deleting ({i} of {items.Count})";
|
||||
notification.Progress = (float)++i / items.Count;
|
||||
Delete(b);
|
||||
}
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore multiple items that were previously deleted.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void Undelete(List<TModel> items)
|
||||
{
|
||||
if (!items.Any()) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
CompletionText = "Restored all deleted items!",
|
||||
Progress = 0,
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
notification.Text = $"Restoring ({i} of {items.Count})";
|
||||
notification.Progress = (float)++i / items.Count;
|
||||
Undelete(item);
|
||||
}
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public void Undelete(TModel item)
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
usage.Context.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
|
||||
if (!ModelStore.Undelete(item)) return;
|
||||
|
||||
Files.Reference(item.Files.Select(f => f.FileInfo).ToArray());
|
||||
|
||||
usage.Context.ChangeTracker.AutoDetectChangesEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create all required <see cref="FileInfo"/>s for the provided archive, adding them to the global file store.
|
||||
/// </summary>
|
||||
private List<TFileModel> createFileInfos(ArchiveReader reader, FileStore files)
|
||||
{
|
||||
var fileInfos = new List<TFileModel>();
|
||||
|
||||
// import files to manager
|
||||
foreach (string file in reader.Filenames)
|
||||
using (Stream s = reader.GetStream(file))
|
||||
fileInfos.Add(new TFileModel
|
||||
{
|
||||
Filename = file,
|
||||
FileInfo = files.Add(s)
|
||||
});
|
||||
|
||||
return fileInfos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a barebones model from the provided archive.
|
||||
/// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking.
|
||||
/// </summary>
|
||||
/// <param name="archive">The archive to create the model for.</param>
|
||||
/// <returns>A model populated with minimal information.</returns>
|
||||
protected abstract TModel CreateModel(ArchiveReader archive);
|
||||
|
||||
/// <summary>
|
||||
/// Populate the provided model completely from the given archive.
|
||||
/// After this method, the model should be in a state ready to commit to a store.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to populate.</param>
|
||||
/// <param name="archive">The archive to use as a reference for population.</param>
|
||||
protected virtual void Populate(TModel model, ArchiveReader archive)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual TModel CheckForExisting(TModel model) => null;
|
||||
|
||||
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
|
||||
/// </summary>
|
||||
/// <param name="path">A file or folder path resolving the archive content.</param>
|
||||
/// <returns>A reader giving access to the archive's content.</returns>
|
||||
private ArchiveReader getReaderFrom(string path)
|
||||
{
|
||||
if (ZipFile.IsZipFile(path))
|
||||
return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path));
|
||||
return new LegacyFilesystemReader(path);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Platform;
|
||||
@ -23,7 +22,7 @@ namespace osu.Game.Database
|
||||
/// <param name="obj">The object to use as a reference when negotiating a local instance.</param>
|
||||
/// <param name="lookupSource">An optional lookup source which will be used to query and populate a freshly retrieved replacement. If not provided, the refreshed object will still be returned but will not have any includes.</param>
|
||||
/// <typeparam name="T">A valid EF-stored type.</typeparam>
|
||||
protected virtual void Refresh<T>(ref T obj, IEnumerable<T> lookupSource = null) where T : class, IHasPrimaryKey
|
||||
protected virtual void Refresh<T>(ref T obj, IQueryable<T> lookupSource = null) where T : class, IHasPrimaryKey
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
|
@ -26,7 +26,8 @@ namespace osu.Game.Database
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a context for read-only usage.
|
||||
/// Get a context for the current thread for read-only usage.
|
||||
/// If a <see cref="DatabaseWriteUsage"/> is in progress, the existing write-safe context will be returned.
|
||||
/// </summary>
|
||||
public OsuDbContext Get() => threadContexts.Value;
|
||||
|
||||
|
22
osu.Game/Database/ICanAcceptFiles.cs
Normal file
22
osu.Game/Database/ICanAcceptFiles.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// A class which can accept files for importing.
|
||||
/// </summary>
|
||||
public interface ICanAcceptFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Import the specified paths.
|
||||
/// </summary>
|
||||
/// <param name="paths">The files which should be imported.</param>
|
||||
void Import(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// An array of accepted file extensions (in the standard format of ".abc").
|
||||
/// </summary>
|
||||
string[] HandledExtensions { get; }
|
||||
}
|
||||
}
|
16
osu.Game/Database/IHasFiles.cs
Normal file
16
osu.Game/Database/IHasFiles.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// A model that contains a list of files it is responsible for.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFile">The model representing a file.</typeparam>
|
||||
public interface IHasFiles<TFile>
|
||||
{
|
||||
List<TFile> Files { get; set; }
|
||||
}
|
||||
}
|
16
osu.Game/Database/INamedFileInfo.cs
Normal file
16
osu.Game/Database/INamedFileInfo.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.IO;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a join model which gives a filename and scope to a <see cref="FileInfo"/>.
|
||||
/// </summary>
|
||||
public interface INamedFileInfo
|
||||
{
|
||||
FileInfo FileInfo { get; set; }
|
||||
string Filename { get; set; }
|
||||
}
|
||||
}
|
16
osu.Game/Database/ISoftDelete.cs
Normal file
16
osu.Game/Database/ISoftDelete.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// A model that can be deleted from user's view without being instantly lost.
|
||||
/// </summary>
|
||||
public interface ISoftDelete
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this model is marked for future deletion.
|
||||
/// </summary>
|
||||
bool DeletePending { get; set; }
|
||||
}
|
||||
}
|
149
osu.Game/Database/MutableDatabaseBackedStore.cs
Normal file
149
osu.Game/Database/MutableDatabaseBackedStore.cs
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// A typed store which supports basic addition, deletion and updating for soft-deletable models.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The databased model.</typeparam>
|
||||
public abstract class MutableDatabaseBackedStore<T> : DatabaseBackedStore
|
||||
where T : class, IHasPrimaryKey, ISoftDelete
|
||||
{
|
||||
public event Action<T> ItemAdded;
|
||||
public event Action<T> ItemRemoved;
|
||||
|
||||
protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
|
||||
: base(contextFactory, storage)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Access items pre-populated with includes for consumption.
|
||||
/// </summary>
|
||||
public IQueryable<T> ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set<T>());
|
||||
|
||||
/// <summary>
|
||||
/// Add a <see cref="T"/> to the database.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to add.</param>
|
||||
public void Add(T item)
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
var context = usage.Context;
|
||||
context.Attach(item);
|
||||
}
|
||||
|
||||
ItemAdded?.Invoke(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a <see cref="T"/> in the database.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to update.</param>
|
||||
public void Update(T item)
|
||||
{
|
||||
ItemRemoved?.Invoke(item);
|
||||
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
usage.Context.Update(item);
|
||||
|
||||
ItemAdded?.Invoke(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a <see cref="T"/> from the database.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to delete.</param>
|
||||
public bool Delete(T item)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
Refresh(ref item);
|
||||
|
||||
if (item.DeletePending) return false;
|
||||
item.DeletePending = true;
|
||||
}
|
||||
|
||||
ItemRemoved?.Invoke(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore a <see cref="T"/> from a deleted state.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to undelete.</param>
|
||||
public bool Undelete(T item)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
Refresh(ref item, ConsumableItems);
|
||||
|
||||
if (!item.DeletePending) return false;
|
||||
item.DeletePending = false;
|
||||
}
|
||||
|
||||
ItemAdded?.Invoke(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allow implementations to add database-side includes or constraints when querying for consumption of items.
|
||||
/// </summary>
|
||||
/// <param name="query">The input query.</param>
|
||||
/// <returns>A potentially modified output query.</returns>
|
||||
protected virtual IQueryable<T> AddIncludesForConsumption(IQueryable<T> query) => query;
|
||||
|
||||
/// <summary>
|
||||
/// Allow implementations to add database-side includes or constraints when deleting items.
|
||||
/// Included properties could then be subsequently deleted by overriding <see cref="Purge"/>.
|
||||
/// </summary>
|
||||
/// <param name="query">The input query.</param>
|
||||
/// <returns>A potentially modified output query.</returns>
|
||||
protected virtual IQueryable<T> AddIncludesForDeletion(IQueryable<T> query) => query;
|
||||
|
||||
/// <summary>
|
||||
/// Called when removing an item completely from the database.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to be purged.</param>
|
||||
/// <param name="context">The write context which can be used to perform subsequent deletions.</param>
|
||||
protected virtual void Purge(List<T> items, OsuDbContext context) => context.RemoveRange(items);
|
||||
|
||||
public override void Cleanup()
|
||||
{
|
||||
base.Cleanup();
|
||||
PurgeDeletable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Purge items in a pending delete state.
|
||||
/// </summary>
|
||||
/// <param name="query">An optional query limiting the scope of the purge.</param>
|
||||
public void PurgeDeletable(Expression<Func<T, bool>> query = null)
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
var context = usage.Context;
|
||||
|
||||
var lookup = context.Set<T>().Where(s => s.DeletePending);
|
||||
|
||||
if (query != null) lookup = lookup.Where(query);
|
||||
|
||||
lookup = AddIncludesForDeletion(lookup);
|
||||
|
||||
var purgeable = lookup.ToList();
|
||||
|
||||
if (!purgeable.Any()) return;
|
||||
|
||||
Purge(purgeable, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using osu.Framework.IO.Stores;
|
||||
|
||||
namespace osu.Game.Beatmaps.IO
|
||||
namespace osu.Game.IO.Archives
|
||||
{
|
||||
public abstract class ArchiveReader : IDisposable, IResourceStore<byte[]>
|
||||
{
|
||||
@ -17,6 +17,16 @@ namespace osu.Game.Beatmaps.IO
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// The name of this archive (usually the containing filename).
|
||||
/// </summary>
|
||||
public readonly string Name;
|
||||
|
||||
protected ArchiveReader(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public abstract IEnumerable<string> Filenames { get; }
|
||||
|
||||
public virtual byte[] Get(string name)
|
@ -1,12 +1,12 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Framework.IO.File;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.IO.File;
|
||||
|
||||
namespace osu.Game.Beatmaps.IO
|
||||
namespace osu.Game.IO.Archives
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads an extracted legacy beatmap from disk.
|
||||
@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps.IO
|
||||
{
|
||||
private readonly string path;
|
||||
|
||||
public LegacyFilesystemReader(string path)
|
||||
public LegacyFilesystemReader(string path) : base(Path.GetFileName(path))
|
||||
{
|
||||
this.path = path;
|
||||
}
|
@ -6,14 +6,15 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using Ionic.Zip;
|
||||
|
||||
namespace osu.Game.Beatmaps.IO
|
||||
namespace osu.Game.IO.Archives
|
||||
{
|
||||
public sealed class OszArchiveReader : ArchiveReader
|
||||
public sealed class ZipArchiveReader : ArchiveReader
|
||||
{
|
||||
private readonly Stream archiveStream;
|
||||
private readonly ZipFile archive;
|
||||
|
||||
public OszArchiveReader(Stream archiveStream)
|
||||
public ZipArchiveReader(Stream archiveStream, string name = null)
|
||||
: base(name)
|
||||
{
|
||||
this.archiveStream = archiveStream;
|
||||
archive = ZipFile.Read(archiveStream);
|
@ -2,23 +2,25 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.IPC
|
||||
{
|
||||
public class BeatmapIPCChannel : IpcChannel<BeatmapImportMessage>
|
||||
public class ArchiveImportIPCChannel : IpcChannel<ArchiveImportMessage>
|
||||
{
|
||||
private readonly BeatmapManager beatmaps;
|
||||
private readonly ICanAcceptFiles importer;
|
||||
|
||||
public BeatmapIPCChannel(IIpcHost host, BeatmapManager beatmaps = null)
|
||||
public ArchiveImportIPCChannel(IIpcHost host, ICanAcceptFiles importer = null)
|
||||
: base(host)
|
||||
{
|
||||
this.beatmaps = beatmaps;
|
||||
this.importer = importer;
|
||||
MessageReceived += msg =>
|
||||
{
|
||||
Debug.Assert(beatmaps != null);
|
||||
Debug.Assert(importer != null);
|
||||
ImportAsync(msg.Path).ContinueWith(t =>
|
||||
{
|
||||
if (t.Exception != null) throw t.Exception;
|
||||
@ -28,18 +30,19 @@ namespace osu.Game.IPC
|
||||
|
||||
public async Task ImportAsync(string path)
|
||||
{
|
||||
if (beatmaps == null)
|
||||
if (importer == null)
|
||||
{
|
||||
//we want to contact a remote osu! to handle the import.
|
||||
await SendMessageAsync(new BeatmapImportMessage { Path = path });
|
||||
await SendMessageAsync(new ArchiveImportMessage { Path = path });
|
||||
return;
|
||||
}
|
||||
|
||||
beatmaps.Import(path);
|
||||
if (importer.HandledExtensions.Contains(Path.GetExtension(path)))
|
||||
importer.Import(path);
|
||||
}
|
||||
}
|
||||
|
||||
public class BeatmapImportMessage
|
||||
public class ArchiveImportMessage
|
||||
{
|
||||
public string Path;
|
||||
}
|
33
osu.Game/Online/API/APIDownloadRequest.cs
Normal file
33
osu.Game/Online/API/APIDownloadRequest.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
{
|
||||
public abstract class APIDownloadRequest : APIRequest
|
||||
{
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var request = new WebRequest(Uri);
|
||||
request.DownloadProgress += request_Progress;
|
||||
return request;
|
||||
}
|
||||
|
||||
private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); });
|
||||
|
||||
protected APIDownloadRequest()
|
||||
{
|
||||
base.Success += onSuccess;
|
||||
}
|
||||
|
||||
private void onSuccess()
|
||||
{
|
||||
Success?.Invoke(WebRequest.ResponseData);
|
||||
}
|
||||
|
||||
public event APIProgressHandler Progress;
|
||||
|
||||
public new event APISuccessHandler<byte[]> Success;
|
||||
}
|
||||
}
|
@ -27,32 +27,6 @@ namespace osu.Game.Online.API
|
||||
public new event APISuccessHandler<T> Success;
|
||||
}
|
||||
|
||||
public abstract class APIDownloadRequest : APIRequest
|
||||
{
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var request = new WebRequest(Uri);
|
||||
request.DownloadProgress += request_Progress;
|
||||
return request;
|
||||
}
|
||||
|
||||
private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); });
|
||||
|
||||
protected APIDownloadRequest()
|
||||
{
|
||||
base.Success += onSuccess;
|
||||
}
|
||||
|
||||
private void onSuccess()
|
||||
{
|
||||
Success?.Invoke(WebRequest.ResponseData);
|
||||
}
|
||||
|
||||
public event APIProgressHandler Progress;
|
||||
|
||||
public new event APISuccessHandler<byte[]> Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AN API request with no specified response type.
|
||||
/// </summary>
|
||||
|
@ -105,6 +105,8 @@ namespace osu.Game
|
||||
{
|
||||
this.frameworkConfig = frameworkConfig;
|
||||
|
||||
ScoreStore.ScoreImported += score => Schedule(() => LoadScore(score));
|
||||
|
||||
if (!Host.IsPrimaryInstance)
|
||||
{
|
||||
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
|
||||
@ -114,7 +116,8 @@ namespace osu.Game
|
||||
if (args?.Length > 0)
|
||||
{
|
||||
var paths = args.Where(a => !a.StartsWith(@"-"));
|
||||
Task.Run(() => BeatmapManager.Import(paths.ToArray()));
|
||||
|
||||
Task.Run(() => Import(paths.ToArray()));
|
||||
}
|
||||
|
||||
dependencies.CacheAs(this);
|
||||
|
@ -2,7 +2,10 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -30,7 +33,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game
|
||||
{
|
||||
public class OsuGameBase : Framework.Game, IOnlineComponent
|
||||
public class OsuGameBase : Framework.Game, IOnlineComponent, ICanAcceptFiles
|
||||
{
|
||||
protected OsuConfigManager LocalConfig;
|
||||
|
||||
@ -114,6 +117,8 @@ namespace osu.Game
|
||||
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
|
||||
dependencies.Cache(new OsuColour());
|
||||
|
||||
fileImporters.Add(BeatmapManager);
|
||||
|
||||
//this completely overrides the framework default. will need to change once we make a proper FontStore.
|
||||
dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 });
|
||||
|
||||
@ -257,5 +262,17 @@ namespace osu.Game
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();
|
||||
|
||||
public void Import(params string[] paths)
|
||||
{
|
||||
var extension = Path.GetExtension(paths.First());
|
||||
|
||||
foreach (var importer in fileImporters)
|
||||
if (importer.HandledExtensions.Contains(extension)) importer.Import(paths);
|
||||
}
|
||||
|
||||
public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -223,13 +223,13 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
tabsBg.Colour = colours.Gray3;
|
||||
this.beatmaps = beatmaps;
|
||||
|
||||
beatmaps.BeatmapSetAdded += handleBeatmapAdd;
|
||||
beatmaps.ItemAdded += handleBeatmapAdd;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
if (beatmaps != null) beatmaps.BeatmapSetAdded -= handleBeatmapAdd;
|
||||
if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd;
|
||||
}
|
||||
|
||||
private void handleBeatmapAdd(BeatmapSetInfo beatmap)
|
||||
|
@ -185,7 +185,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
resultCountsContainer.Colour = colours.Yellow;
|
||||
|
||||
beatmaps.BeatmapSetAdded += setAdded;
|
||||
beatmaps.ItemAdded += setAdded;
|
||||
}
|
||||
|
||||
private void setAdded(BeatmapSetInfo set)
|
||||
|
@ -74,8 +74,8 @@ namespace osu.Game.Overlays.Music
|
||||
},
|
||||
};
|
||||
|
||||
beatmaps.BeatmapSetAdded += list.AddBeatmapSet;
|
||||
beatmaps.BeatmapSetRemoved += list.RemoveBeatmapSet;
|
||||
beatmaps.ItemAdded += list.AddBeatmapSet;
|
||||
beatmaps.ItemRemoved += list.RemoveBeatmapSet;
|
||||
|
||||
list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets();
|
||||
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
|
||||
{
|
||||
deleteButton.Enabled.Value = false;
|
||||
Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true));
|
||||
Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true));
|
||||
}));
|
||||
}
|
||||
},
|
||||
@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
Action = () =>
|
||||
{
|
||||
undeleteButton.Enabled.Value = false;
|
||||
Task.Run(() => beatmaps.UndeleteAll()).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
|
||||
Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using osu.Framework.Platform;
|
||||
@ -14,7 +15,7 @@ using SharpCompress.Compressors.LZMA;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
public class ScoreStore : DatabaseBackedStore
|
||||
public class ScoreStore : DatabaseBackedStore, ICanAcceptFiles
|
||||
{
|
||||
private readonly Storage storage;
|
||||
|
||||
@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
private const string replay_folder = @"replays";
|
||||
|
||||
public event Action<Score> ScoreImported;
|
||||
|
||||
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
|
||||
private ScoreIPCChannel ipc;
|
||||
|
||||
@ -36,6 +39,18 @@ namespace osu.Game.Rulesets.Scoring
|
||||
ipc = new ScoreIPCChannel(importHost, this);
|
||||
}
|
||||
|
||||
public string[] HandledExtensions => new[] { ".osr" };
|
||||
|
||||
public void Import(params string[] paths)
|
||||
{
|
||||
foreach (var path in paths)
|
||||
{
|
||||
var score = ReadReplayFile(path);
|
||||
if (score != null)
|
||||
ScoreImported?.Invoke(score);
|
||||
}
|
||||
}
|
||||
|
||||
public Score ReadReplayFile(string replayFilename)
|
||||
{
|
||||
Score score;
|
||||
@ -159,5 +174,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
return new Replay { Frames = frames };
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.IO;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
@ -62,8 +62,10 @@ namespace osu.Game.Screens.Menu
|
||||
if (setInfo == null)
|
||||
{
|
||||
// we need to import the default menu background beatmap
|
||||
setInfo = beatmaps.Import(new OszArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz")));
|
||||
setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz"));
|
||||
|
||||
setInfo.Protected = true;
|
||||
beatmaps.Update(setInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,9 +75,6 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
welcome = audio.Sample.Get(@"welcome");
|
||||
seeya = audio.Sample.Get(@"seeya");
|
||||
|
||||
if (setInfo.Protected)
|
||||
beatmaps.Delete(setInfo);
|
||||
}
|
||||
|
||||
protected override void OnEntering(Screen last)
|
||||
|
@ -197,8 +197,8 @@ namespace osu.Game.Screens.Select
|
||||
if (osu != null)
|
||||
Ruleset.BindTo(osu.Ruleset);
|
||||
|
||||
this.beatmaps.BeatmapSetAdded += onBeatmapSetAdded;
|
||||
this.beatmaps.BeatmapSetRemoved += onBeatmapSetRemoved;
|
||||
this.beatmaps.ItemAdded += onBeatmapSetAdded;
|
||||
this.beatmaps.ItemRemoved += onBeatmapSetRemoved;
|
||||
this.beatmaps.BeatmapHidden += onBeatmapHidden;
|
||||
this.beatmaps.BeatmapRestored += onBeatmapRestored;
|
||||
|
||||
@ -401,8 +401,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
if (beatmaps != null)
|
||||
{
|
||||
beatmaps.BeatmapSetAdded -= onBeatmapSetAdded;
|
||||
beatmaps.BeatmapSetRemoved -= onBeatmapSetRemoved;
|
||||
beatmaps.ItemAdded -= onBeatmapSetAdded;
|
||||
beatmaps.ItemRemoved -= onBeatmapSetRemoved;
|
||||
beatmaps.BeatmapHidden -= onBeatmapHidden;
|
||||
beatmaps.BeatmapRestored -= onBeatmapRestored;
|
||||
}
|
||||
@ -448,7 +448,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private void carouselBeatmapsLoaded()
|
||||
{
|
||||
if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false)
|
||||
if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false)
|
||||
{
|
||||
Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo);
|
||||
}
|
||||
|
@ -274,13 +274,23 @@
|
||||
<Compile Include="Configuration\SettingsStore.cs" />
|
||||
<Compile Include="Configuration\DatabasedConfigManager.cs" />
|
||||
<Compile Include="Configuration\SpeedChangeVisualisationMethod.cs" />
|
||||
<Compile Include="Database\ArchiveModelManager.cs" />
|
||||
<Compile Include="Database\DatabaseContextFactory.cs" />
|
||||
<Compile Include="Database\DatabaseWriteUsage.cs" />
|
||||
<Compile Include="Database\ICanAcceptFiles.cs" />
|
||||
<Compile Include="Database\IDatabaseContextFactory.cs" />
|
||||
<Compile Include="Database\IHasFiles.cs" />
|
||||
<Compile Include="Database\IHasPrimaryKey.cs" />
|
||||
<Compile Include="Database\INamedFileInfo.cs" />
|
||||
<Compile Include="Database\ISoftDelete.cs" />
|
||||
<Compile Include="Database\MutableDatabaseBackedStore.cs" />
|
||||
<Compile Include="Database\SingletonContextFactory.cs" />
|
||||
<Compile Include="Graphics\Containers\LinkFlowContainer.cs" />
|
||||
<Compile Include="Graphics\Textures\LargeTextureStore.cs" />
|
||||
<Compile Include="IO\Archives\ArchiveReader.cs" />
|
||||
<Compile Include="IO\Archives\LegacyFilesystemReader.cs" />
|
||||
<Compile Include="IO\Archives\ZipArchiveReader.cs" />
|
||||
<Compile Include="Online\API\APIDownloadRequest.cs" />
|
||||
<Compile Include="Online\API\Requests\GetUserRequest.cs" />
|
||||
<Compile Include="Migrations\20180125143340_Settings.cs" />
|
||||
<Compile Include="Migrations\20180125143340_Settings.Designer.cs">
|
||||
@ -371,8 +381,6 @@
|
||||
<Compile Include="Beatmaps\DummyWorkingBeatmap.cs" />
|
||||
<Compile Include="Beatmaps\Formats\Decoder.cs" />
|
||||
<Compile Include="Beatmaps\Formats\LegacyBeatmapDecoder.cs" />
|
||||
<Compile Include="Beatmaps\IO\ArchiveReader.cs" />
|
||||
<Compile Include="Beatmaps\IO\LegacyFilesystemReader.cs" />
|
||||
<Compile Include="Online\API\Requests\GetUserScoresRequest.cs" />
|
||||
<Compile Include="Screens\Edit\Screens\Compose\RadioButtons\DrawableRadioButton.cs" />
|
||||
<Compile Include="Screens\Edit\Screens\Compose\RadioButtons\RadioButton.cs" />
|
||||
@ -387,7 +395,6 @@
|
||||
<Compile Include="Screens\Play\BreaksOverlay\InfoLine.cs" />
|
||||
<Compile Include="Screens\Play\BreaksOverlay\LetterboxOverlay.cs" />
|
||||
<Compile Include="Screens\Play\BreaksOverlay\RemainingTimeCounter.cs" />
|
||||
<Compile Include="Beatmaps\IO\OszArchiveReader.cs" />
|
||||
<Compile Include="Beatmaps\Legacy\LegacyBeatmap.cs" />
|
||||
<Compile Include="Beatmaps\RankStatus.cs" />
|
||||
<Compile Include="Beatmaps\Timing\BreakPeriod.cs" />
|
||||
@ -470,7 +477,7 @@
|
||||
<Compile Include="IO\Serialization\Converters\TypedListConverter.cs" />
|
||||
<Compile Include="IO\Serialization\Converters\Vector2Converter.cs" />
|
||||
<Compile Include="IO\Serialization\IJsonSerializable.cs" />
|
||||
<Compile Include="IPC\BeatmapIPCChannel.cs" />
|
||||
<Compile Include="IPC\ArchiveImportIPCChannel.cs" />
|
||||
<Compile Include="IPC\ScoreIPCChannel.cs" />
|
||||
<Compile Include="Online\API\APIAccess.cs" />
|
||||
<Compile Include="Online\API\APIRequest.cs" />
|
||||
|
Loading…
Reference in New Issue
Block a user