From e0d28564d0d69b4132f6dea94f1e2a162d18c2e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Feb 2018 20:26:49 +0900 Subject: [PATCH 01/22] Move import logic to shared implementation --- osu.Desktop/OsuGameDesktop.cs | 25 ++- osu.Desktop/Program.cs | 2 +- .../Beatmaps/IO/ImportBeatmapTest.cs | 8 +- .../Beatmaps/ArchiveModelImportManager.cs | 181 ++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 194 ++++-------------- osu.Game/Beatmaps/BeatmapSetFileInfo.cs | 3 +- osu.Game/Beatmaps/BeatmapSetInfo.cs | 3 +- osu.Game/Beatmaps/BeatmapStore.cs | 3 +- osu.Game/Beatmaps/ICanImportArchives.cs | 9 + osu.Game/Database/INamedFileInfo.cs | 13 ++ osu.Game/IO/IAddableStore.cs | 14 ++ osu.Game/IO/IHasFiles.cs | 9 + ...CChannel.cs => ArchiveImportIPCChannel.cs} | 21 +- osu.Game/Online/API/APIDownloadRequest.cs | 30 +++ osu.Game/Online/API/APIRequest.cs | 26 --- osu.Game/osu.Game.csproj | 8 +- 16 files changed, 340 insertions(+), 209 deletions(-) create mode 100644 osu.Game/Beatmaps/ArchiveModelImportManager.cs create mode 100644 osu.Game/Beatmaps/ICanImportArchives.cs create mode 100644 osu.Game/Database/INamedFileInfo.cs create mode 100644 osu.Game/IO/IAddableStore.cs create mode 100644 osu.Game/IO/IHasFiles.cs rename osu.Game/IPC/{BeatmapIPCChannel.cs => ArchiveImportIPCChannel.cs} (57%) create mode 100644 osu.Game/Online/API/APIDownloadRequest.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index f37282366a..c563201f0a 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -111,14 +111,23 @@ 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()); + + if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; + + switch (firstExtension) + { + case ".osz": + Task.Factory.StartNew(() => BeatmapManager.Import(filePaths), TaskCreationOptions.LongRunning); + return; + case ".osr": + Task.Run(() => + { + var score = ScoreStore.ReadReplayFile(filePaths.First()); + Schedule(() => LoadScore(score)); + }); + return; + } } private static readonly string[] allowed_extensions = { @".osz", @".osr" }; diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 9760538197..048fe93c11 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -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) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cade50a9f3..6428881b54 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -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().Import(temp); + var manager = osu.Dependencies.Get(); + + manager.Import(temp); + + var imported = manager.GetAllUsableBeatmapSets(); ensureLoaded(osu); diff --git a/osu.Game/Beatmaps/ArchiveModelImportManager.cs b/osu.Game/Beatmaps/ArchiveModelImportManager.cs new file mode 100644 index 0000000000..af0cdad0a3 --- /dev/null +++ b/osu.Game/Beatmaps/ArchiveModelImportManager.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Ionic.Zip; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Beatmaps.IO; +using osu.Game.Database; +using osu.Game.IO; +using osu.Game.IPC; +using osu.Game.Overlays.Notifications; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Beatmaps +{ + public abstract class ArchiveModelImportManager : ICanImportArchives + where TModel : class, IHasFiles + where TFileModel : INamedFileInfo, new() + { + /// + /// Set an endpoint for notifications to be posted to. + /// + public Action PostNotification { protected get; set; } + + public virtual string[] HandledExtensions => new[] { ".zip" }; + + protected readonly FileStore Files; + + protected readonly IDatabaseContextFactory ContextFactory; + + protected readonly IAddableStore ModelStore; + + // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) + private ArchiveImportIPCChannel ipc; + + protected ArchiveModelImportManager(Storage storage, IDatabaseContextFactory contextFactory, IAddableStore modelStore, IIpcHost importHost = null) + { + ContextFactory = contextFactory; + ModelStore = modelStore; + Files = new FileStore(contextFactory, storage); + + if (importHost != null) + ipc = new ArchiveImportIPCChannel(importHost, this); + } + + /// + /// Import one or more from filesystem . + /// This will post notifications tracking progress. + /// + /// One or more beatmap locations on disk. + 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 imported = new List(); + + 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 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)})"); + } + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Import a model from an . + /// + /// The beatmap to be imported. + 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 set info (don't yet add to database) + var model = CreateModel(archive); + + var existing = CheckForExisting(model); + + if (existing != null) return existing; + + model.Files = createFileInfos(archive, Files); + + Populate(model, archive); + + // import to store + ModelStore.Add(model); + + return model; + } + } + + /// + /// Create all required s for the provided archive, adding them to the global file store. + /// + private List createFileInfos(ArchiveReader reader, FileStore files) + { + var fileInfos = new List(); + + // 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; + } + + /// + /// Create a barebones model from the provided archive. + /// Actual expensive population should be done in ; this should just prepare for duplicate checking. + /// + /// + /// + protected abstract TModel CreateModel(ArchiveReader archive); + + /// + /// 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. + /// + /// The model to populate. + /// The archive to use as a reference for population. + protected virtual void Populate(TModel model, ArchiveReader archive) + { + } + + protected virtual TModel CheckForExisting(TModel beatmapSet) => null; + + /// + /// Creates an from a valid storage path. + /// + /// A file or folder path resolving the beatmap content. + /// A reader giving access to the beatmap's content. + private ArchiveReader getReaderFrom(string path) + { + if (ZipFile.IsZipFile(path)) + return new OszArchiveReader(Files.Storage.GetStream(path)); + return new LegacyFilesystemReader(path); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 47773528a6..0a7bf255c5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -7,7 +7,6 @@ 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; @@ -16,8 +15,6 @@ 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.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Notifications; @@ -28,7 +25,7 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// - public partial class BeatmapManager + public partial class BeatmapManager : ArchiveModelImportManager { /// /// Fired when a new becomes available in the database. @@ -60,9 +57,7 @@ namespace osu.Game.Beatmaps /// 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,142 +67,58 @@ namespace osu.Game.Beatmaps private readonly List currentDownloads = new List(); - // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) - private BeatmapIPCChannel ipc; - - /// - /// Set an endpoint for notifications to be posted to. - /// - public Action PostNotification { private get; set; } - /// /// Set a storage with access to an osu-stable install for import purposes. /// public Func 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 = (BeatmapStore)ModelStore; beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); 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(); } - /// - /// Import one or more from filesystem . - /// This will post notifications tracking progress. - /// - /// One or more beatmap locations on disk. - public List Import(params string[] paths) + protected override void Populate(BeatmapSetInfo model, ArchiveReader archive) { - var notification = new ProgressNotification - { - Text = "Beatmap import is initialising...", - CompletionText = "Import successful!", - Progress = 0, - State = ProgressNotificationState.Active, - }; + model.Beatmaps = createBeatmapDifficulties(archive); - PostNotification?.Invoke(notification); - - List imported = new List(); - - int i = 0; - foreach (string path in paths) - { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return imported; - - 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 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)})"); - } - } - - notification.State = ProgressNotificationState.Completed; - return imported; + // remove metadata from difficulties where it matches the set + foreach (BeatmapInfo b in model.Beatmaps) + if (model.Metadata.Equals(b.Metadata)) + b.Metadata = null; } - /// - /// Import a beatmap from an . - /// - /// The beatmap to be imported. - public BeatmapSetInfo Import(ArchiveReader archive) + protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo beatmapSet) { - using (contextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. + // 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) { - // 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; + 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); + } + } + + return null; } /// @@ -313,7 +224,7 @@ namespace osu.Game.Beatmaps /// The beatmap set to delete. public void Delete(BeatmapSetInfo beatmapSet) { - using (var usage = contextFactory.GetForWrite()) + using (var usage = ContextFactory.GetForWrite()) { var context = usage.Context; @@ -325,7 +236,7 @@ namespace osu.Game.Beatmaps if (beatmaps.Delete(beatmapSet)) { if (!beatmapSet.Protected) - files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); + Files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); } context.ChangeTracker.AutoDetectChangesEnabled = true; @@ -376,14 +287,14 @@ namespace osu.Game.Beatmaps if (beatmapSet.Protected) return; - using (var usage = contextFactory.GetForWrite()) + 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()); + Files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); usage.Context.ChangeTracker.AutoDetectChangesEnabled = true; } @@ -415,7 +326,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); @@ -519,19 +430,6 @@ namespace osu.Game.Beatmaps notification.State = ProgressNotificationState.Completed; } - /// - /// Creates an from a valid storage path. - /// - /// A file or folder path resolving the beatmap content. - /// A reader giving access to the beatmap's content. - private ArchiveReader getReaderFrom(string path) - { - if (ZipFile.IsZipFile(path)) - // ReSharper disable once InconsistentlySynchronizedField - return new OszArchiveReader(files.Storage.GetStream(path)); - return new LegacyFilesystemReader(path); - } - /// /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// @@ -546,10 +444,7 @@ namespace osu.Game.Beatmaps return hashable.ComputeSHA2Hash(); } - /// - /// Create a from a provided archive. - /// - 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 +463,6 @@ namespace osu.Game.Beatmaps }; } - /// - /// Create all required s for the provided archive, adding them to the global file store. - /// - private List createFileInfos(ArchiveReader reader, FileStore files) - { - List fileInfos = new List(); - - // 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; - } - /// /// Create all required s for the provided archive. /// diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs index ae4a6772a2..e88af6ed30 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -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; } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 982e41c92c..0566807179 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -5,10 +5,11 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using osu.Game.Database; +using osu.Game.IO; namespace osu.Game.Beatmaps { - public class BeatmapSetInfo : IHasPrimaryKey + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 29373c0715..8bc2dd8b13 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -6,13 +6,14 @@ using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using osu.Game.Database; +using osu.Game.IO; namespace osu.Game.Beatmaps { /// /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing /// - public class BeatmapStore : DatabaseBackedStore + public class BeatmapStore : DatabaseBackedStore, IAddableStore { public event Action BeatmapSetAdded; public event Action BeatmapSetRemoved; diff --git a/osu.Game/Beatmaps/ICanImportArchives.cs b/osu.Game/Beatmaps/ICanImportArchives.cs new file mode 100644 index 0000000000..246c5d04b2 --- /dev/null +++ b/osu.Game/Beatmaps/ICanImportArchives.cs @@ -0,0 +1,9 @@ +namespace osu.Game.Beatmaps +{ + public interface ICanImportArchives + { + void Import(params string[] paths); + + string[] HandledExtensions { get; } + } +} diff --git a/osu.Game/Database/INamedFileInfo.cs b/osu.Game/Database/INamedFileInfo.cs new file mode 100644 index 0000000000..7922c72974 --- /dev/null +++ b/osu.Game/Database/INamedFileInfo.cs @@ -0,0 +1,13 @@ +using osu.Game.IO; + +namespace osu.Game.Database +{ + /// + /// Represent a join model which gives a filename and scope to a . + /// + public interface INamedFileInfo + { + FileInfo FileInfo { get; set; } + string Filename { get; set; } + } +} diff --git a/osu.Game/IO/IAddableStore.cs b/osu.Game/IO/IAddableStore.cs new file mode 100644 index 0000000000..2452dda3b4 --- /dev/null +++ b/osu.Game/IO/IAddableStore.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.IO +{ + public interface IAddableStore + { + /// + /// Add an object to the store. + /// + /// The object to add. + void Add(T item); + } +} diff --git a/osu.Game/IO/IHasFiles.cs b/osu.Game/IO/IHasFiles.cs new file mode 100644 index 0000000000..df313b4eae --- /dev/null +++ b/osu.Game/IO/IHasFiles.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace osu.Game.IO +{ + public interface IHasFiles + { + List Files { get; set; } + } +} diff --git a/osu.Game/IPC/BeatmapIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs similarity index 57% rename from osu.Game/IPC/BeatmapIPCChannel.cs rename to osu.Game/IPC/ArchiveImportIPCChannel.cs index 64e5d526e6..a5859e56a4 100644 --- a/osu.Game/IPC/BeatmapIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -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; namespace osu.Game.IPC { - public class BeatmapIPCChannel : IpcChannel + public class ArchiveImportIPCChannel : IpcChannel { - private readonly BeatmapManager beatmaps; + private readonly ICanImportArchives importer; - public BeatmapIPCChannel(IIpcHost host, BeatmapManager beatmaps = null) + public ArchiveImportIPCChannel(IIpcHost host, ICanImportArchives 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; } diff --git a/osu.Game/Online/API/APIDownloadRequest.cs b/osu.Game/Online/API/APIDownloadRequest.cs new file mode 100644 index 0000000000..f1cbd1eb0b --- /dev/null +++ b/osu.Game/Online/API/APIDownloadRequest.cs @@ -0,0 +1,30 @@ +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 Success; + } +} \ No newline at end of file diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index ce6f3c7c7d..35af8eefd7 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -27,32 +27,6 @@ namespace osu.Game.Online.API public new event APISuccessHandler 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 Success; - } - /// /// AN API request with no specified response type. /// diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 02801eb81f..189886f5d1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -243,6 +243,7 @@ + @@ -270,6 +271,7 @@ + @@ -278,9 +280,13 @@ + + + + @@ -470,7 +476,7 @@ - + From d8f84fcca3a40ccbb652b7fa01145f877aec0ae1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 10:20:23 +0900 Subject: [PATCH 02/22] Give ArchiveReader a filename --- osu.Game/Beatmaps/ArchiveModelImportManager.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Beatmaps/IO/ArchiveReader.cs | 10 ++++++++++ osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs | 2 +- osu.Game/Beatmaps/IO/OszArchiveReader.cs | 3 ++- osu.Game/Screens/Menu/Intro.cs | 2 +- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/ArchiveModelImportManager.cs b/osu.Game/Beatmaps/ArchiveModelImportManager.cs index af0cdad0a3..beb5f47ad2 100644 --- a/osu.Game/Beatmaps/ArchiveModelImportManager.cs +++ b/osu.Game/Beatmaps/ArchiveModelImportManager.cs @@ -174,7 +174,7 @@ namespace osu.Game.Beatmaps private ArchiveReader getReaderFrom(string path) { if (ZipFile.IsZipFile(path)) - return new OszArchiveReader(Files.Storage.GetStream(path)); + return new OszArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path)); return new LegacyFilesystemReader(path); } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 0a7bf255c5..3821d16103 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -171,7 +171,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 OszArchiveReader(stream, beatmapSetInfo.ToString())) Import(archive); downloadNotification.State = ProgressNotificationState.Completed; diff --git a/osu.Game/Beatmaps/IO/ArchiveReader.cs b/osu.Game/Beatmaps/IO/ArchiveReader.cs index 453a03b882..7be03ffb1b 100644 --- a/osu.Game/Beatmaps/IO/ArchiveReader.cs +++ b/osu.Game/Beatmaps/IO/ArchiveReader.cs @@ -17,6 +17,16 @@ namespace osu.Game.Beatmaps.IO public abstract void Dispose(); + /// + /// The name of this archive (usually the containing filename). + /// + public readonly string Name; + + protected ArchiveReader(string name) + { + Name = name; + } + public abstract IEnumerable Filenames { get; } public virtual byte[] Get(string name) diff --git a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs b/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs index 4a85f6f526..e0a54838e0 100644 --- a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs +++ b/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs @@ -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; } diff --git a/osu.Game/Beatmaps/IO/OszArchiveReader.cs b/osu.Game/Beatmaps/IO/OszArchiveReader.cs index e5c971889b..fbac5d79f3 100644 --- a/osu.Game/Beatmaps/IO/OszArchiveReader.cs +++ b/osu.Game/Beatmaps/IO/OszArchiveReader.cs @@ -13,7 +13,8 @@ namespace osu.Game.Beatmaps.IO private readonly Stream archiveStream; private readonly ZipFile archive; - public OszArchiveReader(Stream archiveStream) + public OszArchiveReader(Stream archiveStream, string name = null) + : base(name) { this.archiveStream = archiveStream; archive = ZipFile.Read(archiveStream); diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index 10b08d704d..3298827d25 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -62,7 +62,7 @@ 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 OszArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz")); setInfo.Protected = true; } } From 6ff63c2f0c8455e9c0d699a9a3ee93ec8944d459 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 12:21:11 +0900 Subject: [PATCH 03/22] Move deletion to ArchiveModelImportManager --- osu.Game/Beatmaps/BeatmapManager.cs | 36 +++------------ osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 +- osu.Game/Beatmaps/BeatmapStore.cs | 24 +++++----- .../ArchiveModelImportManager.cs | 45 +++++++++++++++---- .../ICanImportArchives.cs | 2 +- osu.Game/Database/ISoftDelete.cs | 10 +++++ .../IO/{IAddableStore.cs => IMutableStore.cs} | 4 +- osu.Game/IPC/ArchiveImportIPCChannel.cs | 2 +- osu.Game/Screens/Menu/Intro.cs | 5 +-- osu.Game/osu.Game.csproj | 7 +-- 10 files changed, 76 insertions(+), 61 deletions(-) rename osu.Game/{Beatmaps => Database}/ArchiveModelImportManager.cs (79%) rename osu.Game/{Beatmaps => Database}/ICanImportArchives.cs (78%) create mode 100644 osu.Game/Database/ISoftDelete.cs rename osu.Game/IO/{IAddableStore.cs => IMutableStore.cs} (80%) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 3821d16103..802993bc58 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -97,10 +97,10 @@ namespace osu.Game.Beatmaps b.Metadata = null; } - protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo beatmapSet) + protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model) { // check if this beatmap has already been imported and exit early if so - var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == beatmapSet.Hash); + var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == model.Hash); if (existingHashMatch != null) { Undelete(existingHashMatch); @@ -108,9 +108,9 @@ namespace osu.Game.Beatmaps } // check if a set already exists with the same online id - if (beatmapSet.OnlineBeatmapSetID != null) + if (model.OnlineBeatmapSetID != null) { - var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); + var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); if (existingOnlineId != null) { Delete(existingOnlineId); @@ -217,32 +217,6 @@ namespace osu.Game.Beatmaps /// The beatmap set to update. public void Update(BeatmapSetInfo beatmap) => beatmaps.Update(beatmap); - /// - /// Delete a beatmap from the manager. - /// Is a no-op for already deleted beatmaps. - /// - /// The beatmap set to delete. - 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; - } - } - /// /// Restore all beatmaps that were previously deleted. /// This will post notifications tracking progress. @@ -351,7 +325,7 @@ namespace osu.Game.Beatmaps /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); + public List GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending && !s.Protected).ToList(); /// /// Perform a lookup query on available s. diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 0566807179..79983becb0 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -9,7 +9,7 @@ using osu.Game.IO; namespace osu.Game.Beatmaps { - public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 8bc2dd8b13..330b5db853 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing /// - public class BeatmapStore : DatabaseBackedStore, IAddableStore + public class BeatmapStore : DatabaseBackedStore, IMutableStore { public event Action BeatmapSetAdded; public event Action BeatmapSetRemoved; @@ -79,7 +79,7 @@ namespace osu.Game.Beatmaps { Refresh(ref beatmapSet, BeatmapSets); - if (beatmapSet.DeletePending) return false; + if (beatmapSet.Protected || beatmapSet.DeletePending) return false; beatmapSet.DeletePending = true; } @@ -178,17 +178,17 @@ namespace osu.Game.Beatmaps } public IQueryable 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); + .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 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); + .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); } } diff --git a/osu.Game/Beatmaps/ArchiveModelImportManager.cs b/osu.Game/Database/ArchiveModelImportManager.cs similarity index 79% rename from osu.Game/Beatmaps/ArchiveModelImportManager.cs rename to osu.Game/Database/ArchiveModelImportManager.cs index beb5f47ad2..6b780a2866 100644 --- a/osu.Game/Beatmaps/ArchiveModelImportManager.cs +++ b/osu.Game/Database/ArchiveModelImportManager.cs @@ -1,20 +1,22 @@ 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.Beatmaps; using osu.Game.Beatmaps.IO; -using osu.Game.Database; using osu.Game.IO; using osu.Game.IPC; using osu.Game.Overlays.Notifications; using FileInfo = osu.Game.IO.FileInfo; -namespace osu.Game.Beatmaps +namespace osu.Game.Database { public abstract class ArchiveModelImportManager : ICanImportArchives - where TModel : class, IHasFiles + where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : INamedFileInfo, new() { /// @@ -28,12 +30,12 @@ namespace osu.Game.Beatmaps protected readonly IDatabaseContextFactory ContextFactory; - protected readonly IAddableStore ModelStore; + protected readonly IMutableStore ModelStore; // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; - protected ArchiveModelImportManager(Storage storage, IDatabaseContextFactory contextFactory, IAddableStore modelStore, IIpcHost importHost = null) + protected ArchiveModelImportManager(Storage storage, IDatabaseContextFactory contextFactory, IMutableStore modelStore, IIpcHost importHost = null) { ContextFactory = contextFactory; ModelStore = modelStore; @@ -127,6 +129,31 @@ namespace osu.Game.Beatmaps } } + /// + /// Delete a model from the manager. + /// Is a no-op for already deleted models. + /// + /// The model to delete. + public void Delete(TModel model) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + context.ChangeTracker.AutoDetectChangesEnabled = false; + + // re-fetch the model on the import context. + var foundModel = ContextFactory.Get().Set().Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == model.ID); + + if (foundModel.DeletePending || !CheckCanDelete(foundModel)) return; + + if (ModelStore.Delete(foundModel)) + Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray()); + + context.ChangeTracker.AutoDetectChangesEnabled = true; + } + } + /// /// Create all required s for the provided archive, adding them to the global file store. /// @@ -164,13 +191,15 @@ namespace osu.Game.Beatmaps { } - protected virtual TModel CheckForExisting(TModel beatmapSet) => null; + protected virtual TModel CheckForExisting(TModel model) => null; + + protected virtual bool CheckCanDelete(TModel model) => true; /// /// Creates an from a valid storage path. /// - /// A file or folder path resolving the beatmap content. - /// A reader giving access to the beatmap's content. + /// A file or folder path resolving the archive content. + /// A reader giving access to the archive's content. private ArchiveReader getReaderFrom(string path) { if (ZipFile.IsZipFile(path)) diff --git a/osu.Game/Beatmaps/ICanImportArchives.cs b/osu.Game/Database/ICanImportArchives.cs similarity index 78% rename from osu.Game/Beatmaps/ICanImportArchives.cs rename to osu.Game/Database/ICanImportArchives.cs index 246c5d04b2..0f863f3044 100644 --- a/osu.Game/Beatmaps/ICanImportArchives.cs +++ b/osu.Game/Database/ICanImportArchives.cs @@ -1,4 +1,4 @@ -namespace osu.Game.Beatmaps +namespace osu.Game.Database { public interface ICanImportArchives { diff --git a/osu.Game/Database/ISoftDelete.cs b/osu.Game/Database/ISoftDelete.cs new file mode 100644 index 0000000000..19510421c4 --- /dev/null +++ b/osu.Game/Database/ISoftDelete.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + public interface ISoftDelete + { + bool DeletePending { get; set; } + } +} diff --git a/osu.Game/IO/IAddableStore.cs b/osu.Game/IO/IMutableStore.cs similarity index 80% rename from osu.Game/IO/IAddableStore.cs rename to osu.Game/IO/IMutableStore.cs index 2452dda3b4..ced1b29316 100644 --- a/osu.Game/IO/IAddableStore.cs +++ b/osu.Game/IO/IMutableStore.cs @@ -3,12 +3,14 @@ namespace osu.Game.IO { - public interface IAddableStore + public interface IMutableStore { /// /// Add an object to the store. /// /// The object to add. void Add(T item); + + bool Delete(T item); } } diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index a5859e56a4..6e9787ca5a 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -6,7 +6,7 @@ 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 { diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index 3298827d25..e0467d8f84 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -63,7 +63,9 @@ namespace osu.Game.Screens.Menu { // we need to import the default menu background beatmap setInfo = beatmaps.Import(new OszArchiveReader(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) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 189886f5d1..af2b1dfae5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -243,7 +243,6 @@ - @@ -271,20 +270,22 @@ - + + + - + From d340509b1d97fee381ab21f9cf47cc31a762934c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 12:56:22 +0900 Subject: [PATCH 04/22] Move ArchiveReaders to a more global namespace Also moves delete and action logic to a shared implementation --- .../Beatmaps/IO/OszArchiveReaderTest.cs | 8 +- osu.Game/Beatmaps/BeatmapManager.cs | 81 +---------- osu.Game/Beatmaps/BeatmapStore.cs | 88 +----------- ...mportManager.cs => ArchiveModelManager.cs} | 130 ++++++++++++++---- .../Database/MutableDatabaseBackedStore.cs | 76 ++++++++++ .../IO => IO/Archives}/ArchiveReader.cs | 2 +- .../Archives}/LegacyFilesystemReader.cs | 4 +- .../Archives/ZipArchiveReader.cs} | 6 +- osu.Game/IO/IMutableStore.cs | 16 --- osu.Game/Overlays/BeatmapSet/Header.cs | 4 +- osu.Game/Overlays/DirectOverlay.cs | 2 +- osu.Game/Overlays/Music/PlaylistOverlay.cs | 4 +- osu.Game/Screens/Menu/Intro.cs | 4 +- osu.Game/Screens/Select/SongSelect.cs | 8 +- osu.Game/osu.Game.csproj | 10 +- 15 files changed, 210 insertions(+), 233 deletions(-) rename osu.Game/Database/{ArchiveModelImportManager.cs => ArchiveModelManager.cs} (57%) create mode 100644 osu.Game/Database/MutableDatabaseBackedStore.cs rename osu.Game/{Beatmaps/IO => IO/Archives}/ArchiveReader.cs (94%) rename osu.Game/{Beatmaps/IO => IO/Archives}/LegacyFilesystemReader.cs (93%) rename osu.Game/{Beatmaps/IO/OszArchiveReader.cs => IO/Archives/ZipArchiveReader.cs} (86%) delete mode 100644 osu.Game/IO/IMutableStore.cs diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 44eb385e22..7a1c6d9b89 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -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"))) { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 802993bc58..8bc1f72c1f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -12,9 +12,9 @@ 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.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Notifications; @@ -25,23 +25,13 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// - public partial class BeatmapManager : ArchiveModelImportManager + public partial class BeatmapManager : ArchiveModelManager { - /// - /// Fired when a new becomes available in the database. - /// - public event Action BeatmapSetAdded; - /// /// Fired when a single difficulty has been hidden. /// public event Action BeatmapHidden; - /// - /// Fired when a is removed from the database. - /// - public event Action BeatmapSetRemoved; - /// /// Fired when a single difficulty has been restored. /// @@ -76,8 +66,6 @@ namespace osu.Game.Beatmaps : base(storage, contextFactory, new BeatmapStore(contextFactory), importHost) { beatmaps = (BeatmapStore)ModelStore; - beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); - beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); @@ -121,12 +109,6 @@ namespace osu.Game.Beatmaps return null; } - /// - /// Import a beatmap from a . - /// - /// The beatmap to be imported. - public void Import(BeatmapSetInfo beatmapSet) => beatmaps.Add(beatmapSet); - /// /// Downloads a beatmap. /// This will post notifications tracking progress. @@ -171,7 +153,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, beatmapSetInfo.ToString())) + using (var archive = new ZipArchiveReader(stream, beatmapSetInfo.ToString())) Import(archive); downloadNotification.State = ProgressNotificationState.Completed; @@ -217,63 +199,6 @@ namespace osu.Game.Beatmaps /// The beatmap set to update. public void Update(BeatmapSetInfo beatmap) => beatmaps.Update(beatmap); - /// - /// Restore all beatmaps that were previously deleted. - /// This will post notifications tracking progress. - /// - 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; - } - - /// - /// 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. - /// - /// The beatmap to restore - 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; - } - } - /// /// Delete a beatmap difficulty. /// diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 330b5db853..3e4840f4e1 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -6,18 +6,14 @@ using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using osu.Game.Database; -using osu.Game.IO; namespace osu.Game.Beatmaps { /// /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing /// - public class BeatmapStore : DatabaseBackedStore, IMutableStore + public class BeatmapStore : MutableDatabaseBackedStore { - public event Action BeatmapSetAdded; - public event Action BeatmapSetRemoved; - public event Action BeatmapHidden; public event Action BeatmapRestored; @@ -26,88 +22,6 @@ namespace osu.Game.Beatmaps { } - /// - /// Add a to the database. - /// - /// The beatmap to add. - 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()) for matching entries to our criteria. - var contextMetadata = context.Set().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); - } - } - - /// - /// Update a in the database. TODO: This only supports very basic updates currently. - /// - /// The beatmap to update. - public void Update(BeatmapSetInfo beatmapSet) - { - BeatmapSetRemoved?.Invoke(beatmapSet); - - using (var usage = ContextFactory.GetForWrite()) - usage.Context.BeatmapSetInfo.Update(beatmapSet); - - BeatmapSetAdded?.Invoke(beatmapSet); - } - - /// - /// Delete a from the database. - /// - /// The beatmap to delete. - /// Whether the beatmap's was changed. - public bool Delete(BeatmapSetInfo beatmapSet) - { - using (ContextFactory.GetForWrite()) - { - Refresh(ref beatmapSet, BeatmapSets); - - if (beatmapSet.Protected || beatmapSet.DeletePending) return false; - - beatmapSet.DeletePending = true; - } - - BeatmapSetRemoved?.Invoke(beatmapSet); - return true; - } - - /// - /// Restore a previously deleted . - /// - /// The beatmap to restore. - /// Whether the beatmap's was changed. - 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; - } - /// /// Hide a in the database. /// diff --git a/osu.Game/Database/ArchiveModelImportManager.cs b/osu.Game/Database/ArchiveModelManager.cs similarity index 57% rename from osu.Game/Database/ArchiveModelImportManager.cs rename to osu.Game/Database/ArchiveModelManager.cs index 6b780a2866..9c558a6c12 100644 --- a/osu.Game/Database/ArchiveModelImportManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -6,16 +6,21 @@ using Ionic.Zip; using Microsoft.EntityFrameworkCore; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.IO; 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 { - public abstract class ArchiveModelImportManager : ICanImportArchives + /// + /// Encapsulates a model store class to give it import functionality. + /// Adds cross-functionality with to give access to the central file store for the provided model. + /// + /// The model type. + /// The associated file join type. + public abstract class ArchiveModelManager : ICanImportArchives where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : INamedFileInfo, new() { @@ -24,21 +29,35 @@ namespace osu.Game.Database /// public Action PostNotification { protected get; set; } + /// + /// Fired when a new becomes available in the database. + /// + public event Action ItemAdded; + + /// + /// Fired when a is removed from the database. + /// + public event Action ItemRemoved; + public virtual string[] HandledExtensions => new[] { ".zip" }; protected readonly FileStore Files; protected readonly IDatabaseContextFactory ContextFactory; - protected readonly IMutableStore ModelStore; + protected readonly MutableDatabaseBackedStore ModelStore; // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; - protected ArchiveModelImportManager(Storage storage, IDatabaseContextFactory contextFactory, IMutableStore modelStore, IIpcHost importHost = null) + protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStore 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) @@ -46,10 +65,10 @@ namespace osu.Game.Database } /// - /// Import one or more from filesystem . + /// Import one or more items from filesystem . /// This will post notifications tracking progress. /// - /// One or more beatmap locations on disk. + /// One or more archive locations on disk. public void Import(params string[] paths) { var notification = new ProgressNotification @@ -80,7 +99,7 @@ namespace osu.Game.Database 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. + // 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 @@ -96,7 +115,7 @@ namespace osu.Game.Database catch (Exception e) { e = e.InnerException ?? e; - Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})"); + Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})"); } } @@ -104,37 +123,43 @@ namespace osu.Game.Database } /// - /// Import a model from an . + /// Import an item from an . /// - /// The beatmap to be imported. + /// The archive to be imported. 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 set info (don't yet add to database) - var model = CreateModel(archive); + // create a new model (don't yet add to database) + var item = CreateModel(archive); - var existing = CheckForExisting(model); + var existing = CheckForExisting(item); if (existing != null) return existing; - model.Files = createFileInfos(archive, Files); + item.Files = createFileInfos(archive, Files); - Populate(model, archive); + Populate(item, archive); // import to store - ModelStore.Add(model); + ModelStore.Add(item); - return model; + return item; } } /// - /// Delete a model from the manager. - /// Is a no-op for already deleted models. + /// Import an item from a . /// - /// The model to delete. - public void Delete(TModel model) + /// The model to be imported. + public void Import(TModel item) => ModelStore.Add(item); + + /// + /// Delete an item from the manager. + /// Is a no-op for already deleted items. + /// + /// The item to delete. + public void Delete(TModel item) { using (var usage = ContextFactory.GetForWrite()) { @@ -143,9 +168,9 @@ namespace osu.Game.Database context.ChangeTracker.AutoDetectChangesEnabled = false; // re-fetch the model on the import context. - var foundModel = ContextFactory.Get().Set().Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == model.ID); + var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == item.ID); - if (foundModel.DeletePending || !CheckCanDelete(foundModel)) return; + if (foundModel.DeletePending) return; if (ModelStore.Delete(foundModel)) Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray()); @@ -154,6 +179,59 @@ namespace osu.Game.Database } } + /// + /// Restore all items that were previously deleted. + /// This will post notifications tracking progress. + /// + public void UndeleteAll() + { + var deletedItems = queryModel().Where(m => m.DeletePending).ToList(); + + if (!deletedItems.Any()) return; + + var notification = new ProgressNotification + { + CompletionText = "Restored all deleted items!", + Progress = 0, + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + + foreach (var item in deletedItems) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Restoring ({i} of {deletedItems.Count})"; + notification.Progress = (float)++i / deletedItems.Count; + Undelete(item); + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// 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. + /// + /// The item to restore + 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; + } + } + /// /// Create all required s for the provided archive, adding them to the global file store. /// @@ -193,7 +271,7 @@ namespace osu.Game.Database protected virtual TModel CheckForExisting(TModel model) => null; - protected virtual bool CheckCanDelete(TModel model) => true; + private DbSet queryModel() => ContextFactory.Get().Set(); /// /// Creates an from a valid storage path. @@ -203,7 +281,7 @@ namespace osu.Game.Database private ArchiveReader getReaderFrom(string path) { if (ZipFile.IsZipFile(path)) - return new OszArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path)); + return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path)); return new LegacyFilesystemReader(path); } } diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs new file mode 100644 index 0000000000..c6af1aa475 --- /dev/null +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -0,0 +1,76 @@ +using System; +using osu.Framework.Platform; + +namespace osu.Game.Database +{ + /// + /// A typed store which supports basic addition, deletion and updating for soft-deletable models. + /// + /// The databased model. + public abstract class MutableDatabaseBackedStore : DatabaseBackedStore + where T : class, IHasPrimaryKey, ISoftDelete + { + public event Action ItemAdded; + public event Action ItemRemoved; + + protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) + : base(contextFactory, storage) + { + } + + public void Add(T item) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + context.Attach(item); + } + + ItemAdded?.Invoke(item); + } + + /// + /// Update a in the database. + /// + /// The item to update. + public void Update(T item) + { + ItemRemoved?.Invoke(item); + + using (var usage = ContextFactory.GetForWrite()) + usage.Context.Update(item); + + ItemAdded?.Invoke(item); + } + + public bool Delete(T item) + { + using (ContextFactory.GetForWrite()) + { + Refresh(ref item); + + if (item.DeletePending) return false; + + item.DeletePending = true; + } + + ItemRemoved?.Invoke(item); + return true; + } + + public bool Undelete(T item) + { + using (ContextFactory.GetForWrite()) + { + Refresh(ref item); + + if (!item.DeletePending) return false; + + item.DeletePending = false; + } + + ItemAdded?.Invoke(item); + return true; + } + } +} diff --git a/osu.Game/Beatmaps/IO/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs similarity index 94% rename from osu.Game/Beatmaps/IO/ArchiveReader.cs rename to osu.Game/IO/Archives/ArchiveReader.cs index 7be03ffb1b..351a6dff39 100644 --- a/osu.Game/Beatmaps/IO/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -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 { diff --git a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs b/osu.Game/IO/Archives/LegacyFilesystemReader.cs similarity index 93% rename from osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs rename to osu.Game/IO/Archives/LegacyFilesystemReader.cs index e0a54838e0..d6d80783db 100644 --- a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs +++ b/osu.Game/IO/Archives/LegacyFilesystemReader.cs @@ -1,12 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // 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 { /// /// Reads an extracted legacy beatmap from disk. diff --git a/osu.Game/Beatmaps/IO/OszArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs similarity index 86% rename from osu.Game/Beatmaps/IO/OszArchiveReader.cs rename to osu.Game/IO/Archives/ZipArchiveReader.cs index fbac5d79f3..a772382b5e 100644 --- a/osu.Game/Beatmaps/IO/OszArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -6,14 +6,14 @@ 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, string name = null) + public ZipArchiveReader(Stream archiveStream, string name = null) : base(name) { this.archiveStream = archiveStream; diff --git a/osu.Game/IO/IMutableStore.cs b/osu.Game/IO/IMutableStore.cs deleted file mode 100644 index ced1b29316..0000000000 --- a/osu.Game/IO/IMutableStore.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -namespace osu.Game.IO -{ - public interface IMutableStore - { - /// - /// Add an object to the store. - /// - /// The object to add. - void Add(T item); - - bool Delete(T item); - } -} diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 36b6a9964a..3ce0dfee31 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -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) diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 05b5bba09c..8d8a4aebaa 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -185,7 +185,7 @@ namespace osu.Game.Overlays resultCountsContainer.Colour = colours.Yellow; - beatmaps.BeatmapSetAdded += setAdded; + beatmaps.ItemAdded += setAdded; } private void setAdded(BeatmapSetInfo set) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 2125984785..ac7ec6257b 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -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(); diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index e0467d8f84..ce3c93ebcf 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -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,7 +62,7 @@ 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"), "circles.osz")); + setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz")); setInfo.Protected = true; beatmaps.Update(setInfo); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2421a4fdfe..f35768d933 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -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; } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index af2b1dfae5..91aaf9c092 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -274,7 +274,7 @@ - + @@ -282,10 +282,13 @@ + - + + + @@ -378,8 +381,6 @@ - - @@ -394,7 +395,6 @@ - From d3dd31dadb0d8312475bc2fc82c4595340217582 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 13:30:17 +0900 Subject: [PATCH 05/22] Make deletion and purging logic even more global --- .../Visual/TestCasePlaySongSelect.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 52 +------------- osu.Game/Beatmaps/BeatmapStore.cs | 38 ++++------- osu.Game/Database/ArchiveModelManager.cs | 68 ++++++++++++++++--- osu.Game/Database/DatabaseBackedStore.cs | 2 +- .../Database/MutableDatabaseBackedStore.cs | 35 ++++++++++ osu.Game/IO/FileStore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- .../Sections/Maintenance/GeneralSettings.cs | 4 +- 9 files changed, 114 insertions(+), 91 deletions(-) diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 8bb0d152f6..13b2be9fdb 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual { if (deleteMaps) { - manager.DeleteAll(); + manager.Delete(manager.GetAllUsableBeatmapSets()); game.Beatmap.SetDefault(); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 8bc1f72c1f..4a6b6909b9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -71,8 +71,6 @@ namespace osu.Game.Beatmaps this.rulesets = rulesets; this.api = api; - - beatmaps.Cleanup(); } protected override void Populate(BeatmapSetInfo model, ArchiveReader archive) @@ -102,7 +100,7 @@ namespace osu.Game.Beatmaps if (existingOnlineId != null) { Delete(existingOnlineId); - beatmaps.Cleanup(s => s.ID == existingOnlineId.ID); + beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); } } @@ -193,12 +191,6 @@ namespace osu.Game.Beatmaps /// The object if it exists, or null. public DownloadBeatmapSetRequest GetExistingDownload(BeatmapSetInfo beatmap) => currentDownloads.Find(d => d.BeatmapSet.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID); - /// - /// Update a BeatmapSetInfo with all changes. TODO: This only supports very basic updates currently. - /// - /// The beatmap set to update. - public void Update(BeatmapSetInfo beatmap) => beatmaps.Update(beatmap); - /// /// Delete a beatmap difficulty. /// @@ -239,13 +231,6 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query); - /// - /// Refresh an existing instance of a from the store. - /// - /// A stale instance. - /// A fresh instance. - public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID); - /// /// Returns a list of all usable s. /// @@ -294,41 +279,6 @@ namespace osu.Game.Beatmaps await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); } - /// - /// Delete all beatmaps. - /// This will post notifications tracking progress. - /// - 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; - } - /// /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 3e4840f4e1..e695c3bf28 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -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; @@ -63,32 +63,24 @@ namespace osu.Game.Beatmaps return true; } - public override void Cleanup() => Cleanup(_ => true); - - public void Cleanup(Expression> query) + protected override IQueryable AddIncludesForDeletion(IQueryable query) { - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; + return base.AddIncludesForDeletion(query) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Metadata); + } - 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(); + protected override void Purge(List items, OsuDbContext 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))); - if (!purgeable.Any()) return; + // 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))); - // 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 BeatmapSets => ContextFactory.Get().BeatmapSetInfo diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9c558a6c12..31eab79127 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -62,6 +62,8 @@ namespace osu.Game.Database if (importHost != null) ipc = new ArchiveImportIPCChannel(importHost, this); + + ModelStore.PurgeDeletable(); } /// @@ -154,6 +156,13 @@ namespace osu.Game.Database /// The model to be imported. public void Import(TModel item) => ModelStore.Add(item); + /// + /// Perform an update of the specified item. + /// TODO: Support file changes. + /// + /// The item to update. + public void Update(TModel item) => ModelStore.Update(item); + /// /// Delete an item from the manager. /// Is a no-op for already deleted items. @@ -180,14 +189,48 @@ namespace osu.Game.Database } /// - /// Restore all items that were previously deleted. + /// Delete multiple items. /// This will post notifications tracking progress. /// - public void UndeleteAll() + public void Delete(List items) { - var deletedItems = queryModel().Where(m => m.DeletePending).ToList(); + if (items.Count == 0) return; - if (!deletedItems.Any()) 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; + } + + /// + /// Restore multiple items that were previously deleted. + /// This will post notifications tracking progress. + /// + public void Undelete(List items) + { + if (!items.Any()) return; var notification = new ProgressNotification { @@ -200,15 +243,18 @@ namespace osu.Game.Database int i = 0; - foreach (var item in deletedItems) + using (ContextFactory.GetForWrite()) { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; + foreach (var item in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; - notification.Text = $"Restoring ({i} of {deletedItems.Count})"; - notification.Progress = (float)++i / deletedItems.Count; - Undelete(item); + notification.Text = $"Restoring ({i} of {items.Count})"; + notification.Progress = (float)++i / items.Count; + Undelete(item); + } } notification.State = ProgressNotificationState.Completed; diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index cf46b66422..a1ed992f03 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -49,7 +49,7 @@ namespace osu.Game.Database /// /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. /// - public virtual void Cleanup() + public virtual void PurgeDeletable() { } } diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index c6af1aa475..9de6068d10 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using osu.Framework.Platform; namespace osu.Game.Database @@ -72,5 +75,37 @@ namespace osu.Game.Database ItemAdded?.Invoke(item); return true; } + + protected virtual IQueryable AddIncludesForDeletion(IQueryable query) => query; + + protected virtual void Purge(List items, OsuDbContext context) + { + // cascades down to beatmaps. + context.RemoveRange(items); + } + + /// + /// Purge items in a pending delete state. + /// + /// An optional query limiting the scope of the purge. + public void PurgeDeletable(Expression> query = null) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + var lookup = context.Set().Where(s => s.DeletePending); + + if (query != null) lookup = lookup.Where(query); + + AddIncludesForDeletion(lookup); + + var purgeable = lookup.ToList(); + + if (!purgeable.Any()) return; + + Purge(purgeable, context); + } + } } } diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index ab81ba4851..6f262fd8fa 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -90,7 +90,7 @@ namespace osu.Game.IO } } - public override void Cleanup() + public override void PurgeDeletable() { using (var usage = ContextFactory.GetForWrite()) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 505577416d..ce50f160f7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -172,7 +172,7 @@ namespace osu.Game API.Register(this); - FileStore.Cleanup(); + FileStore.PurgeDeletable(); } private void runMigrations() diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 1223310c74..eec99dc886 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -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.QueryBeatmapSet(b => b.DeletePending))).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); } }, }; From a0a65abcac810041ac20cc78134f4daf758a7961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 14:19:16 +0900 Subject: [PATCH 06/22] Crentalise all import logic --- osu.Desktop/OsuGameDesktop.cs | 16 +--------------- osu.Game/Database/ArchiveModelManager.cs | 2 +- ...anImportArchives.cs => ICanAcceptFiles.cs} | 2 +- osu.Game/IPC/ArchiveImportIPCChannel.cs | 4 ++-- osu.Game/OsuGame.cs | 5 ++++- osu.Game/OsuGameBase.cs | 19 ++++++++++++++++++- osu.Game/Rulesets/Scoring/ScoreStore.cs | 18 +++++++++++++++++- osu.Game/osu.Game.csproj | 2 +- 8 files changed, 45 insertions(+), 23 deletions(-) rename osu.Game/Database/{ICanImportArchives.cs => ICanAcceptFiles.cs} (73%) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index c563201f0a..45ed66bad2 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -115,21 +115,7 @@ namespace osu.Desktop if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; - switch (firstExtension) - { - case ".osz": - Task.Factory.StartNew(() => BeatmapManager.Import(filePaths), TaskCreationOptions.LongRunning); - return; - case ".osr": - Task.Run(() => - { - var score = ScoreStore.ReadReplayFile(filePaths.First()); - Schedule(() => LoadScore(score)); - }); - return; - } + Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); } - - private static readonly string[] allowed_extensions = { @".osz", @".osr" }; } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 31eab79127..1b37e7e76c 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -20,7 +20,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanImportArchives + public abstract class ArchiveModelManager : ICanAcceptFiles where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : INamedFileInfo, new() { diff --git a/osu.Game/Database/ICanImportArchives.cs b/osu.Game/Database/ICanAcceptFiles.cs similarity index 73% rename from osu.Game/Database/ICanImportArchives.cs rename to osu.Game/Database/ICanAcceptFiles.cs index 0f863f3044..d09000525d 100644 --- a/osu.Game/Database/ICanImportArchives.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -1,6 +1,6 @@ namespace osu.Game.Database { - public interface ICanImportArchives + public interface ICanAcceptFiles { void Import(params string[] paths); diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index 6e9787ca5a..9d7bf17c77 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -12,9 +12,9 @@ namespace osu.Game.IPC { public class ArchiveImportIPCChannel : IpcChannel { - private readonly ICanImportArchives importer; + private readonly ICanAcceptFiles importer; - public ArchiveImportIPCChannel(IIpcHost host, ICanImportArchives importer = null) + public ArchiveImportIPCChannel(IIpcHost host, ICanAcceptFiles importer = null) : base(host) { this.importer = importer; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 624179cfe1..14bc31aecf 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -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); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ce50f160f7..dba0250007 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -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 fileImporters = new List(); + + 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(); } } diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index 8bde2747a2..7abee0b04f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // 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 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 }; } + } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 91aaf9c092..bfe7ec1821 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -277,7 +277,7 @@ - + From fe5df663be1c648eb4c0c41398d447141e8eaa04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 15:14:46 +0900 Subject: [PATCH 07/22] Add more xmldoc --- osu.Game/Database/ICanAcceptFiles.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index d09000525d..40978277b1 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -1,9 +1,19 @@ namespace osu.Game.Database { + /// + /// A class which can accept files for importing. + /// public interface ICanAcceptFiles { + /// + /// Import the specified paths. + /// + /// The files which should be imported. void Import(params string[] paths); + /// + /// An array of accepted file extensions (in the standard format of ".abc"). + /// string[] HandledExtensions { get; } } } From d07ab1fbea0f25ee28d7e1dfc2e2a08d690ff1bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 15:14:55 +0900 Subject: [PATCH 08/22] Fix undelete all --- .../Overlays/Settings/Sections/Maintenance/GeneralSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index eec99dc886..d9fedd0225 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { undeleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.DeletePending))).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); + Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); } }, }; From e51450a0645604d03af8fb1d610e84e85b3634e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 15:24:28 +0900 Subject: [PATCH 09/22] Fix query construction --- osu.Game/Database/MutableDatabaseBackedStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index 9de6068d10..887f568864 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -98,7 +98,7 @@ namespace osu.Game.Database if (query != null) lookup = lookup.Where(query); - AddIncludesForDeletion(lookup); + lookup = AddIncludesForDeletion(lookup); var purgeable = lookup.ToList(); From 671475f3b40981f2105283da8aadec8a86b3952a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 15:51:59 +0900 Subject: [PATCH 10/22] Ensure undeleted items are populated with includes before firing events --- osu.Game/Database/DatabaseBackedStore.cs | 3 +-- osu.Game/Database/MutableDatabaseBackedStore.cs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index a1ed992f03..6109475690 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // 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 /// The object to use as a reference when negotiating a local instance. /// 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. /// A valid EF-stored type. - protected virtual void Refresh(ref T obj, IEnumerable lookupSource = null) where T : class, IHasPrimaryKey + protected virtual void Refresh(ref T obj, IQueryable lookupSource = null) where T : class, IHasPrimaryKey { using (var usage = ContextFactory.GetForWrite()) { diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index 887f568864..01fcfbfe43 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -53,7 +53,6 @@ namespace osu.Game.Database Refresh(ref item); if (item.DeletePending) return false; - item.DeletePending = true; } @@ -65,10 +64,9 @@ namespace osu.Game.Database { using (ContextFactory.GetForWrite()) { - Refresh(ref item); + Refresh(ref item, ConsumableItems); if (!item.DeletePending) return false; - item.DeletePending = false; } @@ -76,6 +74,8 @@ namespace osu.Game.Database return true; } + protected virtual IQueryable AddIncludesForConsumption(IQueryable query) => query; + protected virtual IQueryable AddIncludesForDeletion(IQueryable query) => query; protected virtual void Purge(List items, OsuDbContext context) From 89cf794f980e4ac6137b234e8850764c67cbc477 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 15:52:17 +0900 Subject: [PATCH 11/22] Add a lower level ConsumableItems implementation --- osu.Game/Beatmaps/BeatmapManager.cs | 10 +++--- osu.Game/Beatmaps/BeatmapStore.cs | 34 +++++++++---------- .../Database/MutableDatabaseBackedStore.cs | 5 +++ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4a6b6909b9..1d6d8b6726 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model) { // check if this beatmap has already been imported and exit early if so - var existingHashMatch = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == model.Hash); + var existingHashMatch = beatmaps.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); if (existingHashMatch != null) { Undelete(existingHashMatch); @@ -96,7 +96,7 @@ namespace osu.Game.Beatmaps // check if a set already exists with the same online id if (model.OnlineBeatmapSetID != null) { - var existingOnlineId = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); + var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); if (existingOnlineId != null) { Delete(existingOnlineId); @@ -229,20 +229,20 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query); + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); /// /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending && !s.Protected).ToList(); + public List GetAllUsableBeatmapSets() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected).ToList(); /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().Where(query); + public IEnumerable QueryBeatmapSets(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().Where(query); /// /// Perform a lookup query on available s. diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index e695c3bf28..2e37076aca 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -63,13 +63,19 @@ namespace osu.Game.Beatmaps return true; } - protected override IQueryable AddIncludesForDeletion(IQueryable query) - { - return base.AddIncludesForDeletion(query) + protected override IQueryable AddIncludesForDeletion(IQueryable query) => + base.AddIncludesForDeletion(query) .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) .Include(s => s.Metadata); - } + + protected override IQueryable AddIncludesForConsumption(IQueryable 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 items, OsuDbContext context) { @@ -83,18 +89,12 @@ namespace osu.Game.Beatmaps base.Purge(items, context); } - public IQueryable 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 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 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); } } diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index 01fcfbfe43..3905942c8c 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -21,6 +21,11 @@ namespace osu.Game.Database { } + /// + /// Access items pre-populated with includes for consumption. + /// + public IQueryable ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set()); + public void Add(T item) { using (var usage = ContextFactory.GetForWrite()) From 8c1d581fb34425849a373e25f92072337b724a4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 16:15:30 +0900 Subject: [PATCH 12/22] Fix hiding beatmaps not refreshing correctly --- osu.Game/Beatmaps/BeatmapStore.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 2e37076aca..93ad1badd2 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -34,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; } @@ -55,7 +53,6 @@ namespace osu.Game.Beatmaps Refresh(ref beatmap, Beatmaps); if (!beatmap.Hidden) return false; - beatmap.Hidden = false; } From 1b13be1372c7ab2e7983473c18a35c9904309363 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 16:23:34 +0900 Subject: [PATCH 13/22] Cleanups and xmldoc additions --- osu.Game/Beatmaps/BeatmapSetInfo.cs | 1 - osu.Game/Database/IHasFiles.cs | 13 +++++++++++++ osu.Game/Database/ISoftDelete.cs | 6 ++++++ osu.Game/Database/MutableDatabaseBackedStore.cs | 6 +----- osu.Game/IO/IHasFiles.cs | 9 --------- osu.Game/osu.Game.csproj | 2 +- 6 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Database/IHasFiles.cs delete mode 100644 osu.Game/IO/IHasFiles.cs diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 79983becb0..1736e3fa90 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using osu.Game.Database; -using osu.Game.IO; namespace osu.Game.Beatmaps { diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs new file mode 100644 index 0000000000..cae8ea66ef --- /dev/null +++ b/osu.Game/Database/IHasFiles.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace osu.Game.Database +{ + /// + /// A model that contains a list of files it is responsible for. + /// + /// The model representing a file. + public interface IHasFiles + { + List Files { get; set; } + } +} diff --git a/osu.Game/Database/ISoftDelete.cs b/osu.Game/Database/ISoftDelete.cs index 19510421c4..c884d7af00 100644 --- a/osu.Game/Database/ISoftDelete.cs +++ b/osu.Game/Database/ISoftDelete.cs @@ -3,8 +3,14 @@ namespace osu.Game.Database { + /// + /// A model that can be deleted from user's view without being instantly lost. + /// public interface ISoftDelete { + /// + /// Whether this model is marked for future deletion. + /// bool DeletePending { get; set; } } } diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index 3905942c8c..96bc48fd8a 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -83,11 +83,7 @@ namespace osu.Game.Database protected virtual IQueryable AddIncludesForDeletion(IQueryable query) => query; - protected virtual void Purge(List items, OsuDbContext context) - { - // cascades down to beatmaps. - context.RemoveRange(items); - } + protected virtual void Purge(List items, OsuDbContext context) => context.RemoveRange(items); /// /// Purge items in a pending delete state. diff --git a/osu.Game/IO/IHasFiles.cs b/osu.Game/IO/IHasFiles.cs deleted file mode 100644 index df313b4eae..0000000000 --- a/osu.Game/IO/IHasFiles.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace osu.Game.IO -{ - public interface IHasFiles - { - List Files { get; set; } - } -} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index bfe7ec1821..7e440dacf8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -279,6 +279,7 @@ + @@ -289,7 +290,6 @@ - From b9ef32b09bfb137eafaa034f2cd3e5524f496a4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 16:31:42 +0900 Subject: [PATCH 14/22] Further xmldoc and restoring of Cleanup method --- osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/Database/DatabaseBackedStore.cs | 2 +- .../Database/MutableDatabaseBackedStore.cs | 34 +++++++++++++++++++ osu.Game/IO/FileStore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 1b37e7e76c..20f90a248b 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -63,7 +63,7 @@ namespace osu.Game.Database if (importHost != null) ipc = new ArchiveImportIPCChannel(importHost, this); - ModelStore.PurgeDeletable(); + ModelStore.Cleanup(); } /// diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index 6109475690..0fafb77339 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -48,7 +48,7 @@ namespace osu.Game.Database /// /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. /// - public virtual void PurgeDeletable() + public virtual void Cleanup() { } } diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index 96bc48fd8a..95d3dfc582 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -26,6 +26,10 @@ namespace osu.Game.Database /// public IQueryable ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set()); + /// + /// Add a to the database. + /// + /// The item to add. public void Add(T item) { using (var usage = ContextFactory.GetForWrite()) @@ -51,6 +55,10 @@ namespace osu.Game.Database ItemAdded?.Invoke(item); } + /// + /// Delete a from the database. + /// + /// The item to delete. public bool Delete(T item) { using (ContextFactory.GetForWrite()) @@ -65,6 +73,10 @@ namespace osu.Game.Database return true; } + /// + /// Restore a from a deleted state. + /// + /// The item to undelete. public bool Undelete(T item) { using (ContextFactory.GetForWrite()) @@ -79,12 +91,34 @@ namespace osu.Game.Database return true; } + /// + /// Allow implementations to add database-side includes or constraints when querying for consumption of items. + /// + /// The input query. + /// A potentially modified output query. protected virtual IQueryable AddIncludesForConsumption(IQueryable query) => query; + /// + /// Allow implementations to add database-side includes or constraints when deleting items. + /// Included properties could then be subsequently deleted by overriding . + /// + /// The input query. + /// A potentially modified output query. protected virtual IQueryable AddIncludesForDeletion(IQueryable query) => query; + /// + /// Called when removing an item completely from the database. + /// + /// The items to be purged. + /// The write context which can be used to perform subsequent deletions. protected virtual void Purge(List items, OsuDbContext context) => context.RemoveRange(items); + public override void Cleanup() + { + base.Cleanup(); + PurgeDeletable(); + } + /// /// Purge items in a pending delete state. /// diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 6f262fd8fa..ab81ba4851 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -90,7 +90,7 @@ namespace osu.Game.IO } } - public override void PurgeDeletable() + public override void Cleanup() { using (var usage = ContextFactory.GetForWrite()) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dba0250007..de2a4d0b82 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -177,7 +177,7 @@ namespace osu.Game API.Register(this); - FileStore.PurgeDeletable(); + FileStore.Cleanup(); } private void runMigrations() From fa05822d7dd78ca6fc24dca9843cd762084e4859 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Feb 2018 16:33:33 +0900 Subject: [PATCH 15/22] Add missing licence headers --- osu.Game/Database/ArchiveModelManager.cs | 5 ++++- osu.Game/Database/ICanAcceptFiles.cs | 5 ++++- osu.Game/Database/IHasFiles.cs | 5 ++++- osu.Game/Database/INamedFileInfo.cs | 5 ++++- osu.Game/Database/MutableDatabaseBackedStore.cs | 5 ++++- osu.Game/Online/API/APIDownloadRequest.cs | 7 +++++-- 6 files changed, 25 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 20f90a248b..902a42c172 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) 2007-2018 ppy Pty Ltd . +// 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; diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index 40978277b1..ab26525619 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -1,4 +1,7 @@ -namespace osu.Game.Database +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database { /// /// A class which can accept files for importing. diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs index cae8ea66ef..deaf75360c 100644 --- a/osu.Game/Database/IHasFiles.cs +++ b/osu.Game/Database/IHasFiles.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; namespace osu.Game.Database { diff --git a/osu.Game/Database/INamedFileInfo.cs b/osu.Game/Database/INamedFileInfo.cs index 7922c72974..8de451dd78 100644 --- a/osu.Game/Database/INamedFileInfo.cs +++ b/osu.Game/Database/INamedFileInfo.cs @@ -1,4 +1,7 @@ -using osu.Game.IO; +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.IO; namespace osu.Game.Database { diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index 95d3dfc582..4ab55691f2 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) 2007-2018 ppy Pty Ltd . +// 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; diff --git a/osu.Game/Online/API/APIDownloadRequest.cs b/osu.Game/Online/API/APIDownloadRequest.cs index f1cbd1eb0b..2dff07a847 100644 --- a/osu.Game/Online/API/APIDownloadRequest.cs +++ b/osu.Game/Online/API/APIDownloadRequest.cs @@ -1,4 +1,7 @@ -using osu.Framework.IO.Network; +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.IO.Network; namespace osu.Game.Online.API { @@ -27,4 +30,4 @@ namespace osu.Game.Online.API public new event APISuccessHandler Success; } -} \ No newline at end of file +} From ddf49c2e65e6e2af48c7ebfd3d69df4eb26387c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Feb 2018 10:45:15 +0900 Subject: [PATCH 16/22] Fix intro not being replaced by a playable song when entering song select --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f35768d933..de6847d866 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -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); } From 29adedfa96bf0882f4f54b4727069eca8a35cbac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Feb 2018 11:17:28 +0900 Subject: [PATCH 17/22] Collapse visual settings by default in player --- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 +- .../PlayerSettings/PlayerSettingsGroup.cs | 41 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index e6cf1f7982..5dba10ffc1 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play.HUD //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings(), - VisualSettings = new VisualSettings() + VisualSettings = new VisualSettings { Expanded = false } } }; diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index e8a4bc6b27..95b464154a 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -31,6 +31,28 @@ namespace osu.Game.Screens.Play.PlayerSettings private bool expanded = true; + public bool Expanded + { + get { return expanded; } + set + { + if (expanded == value) return; + expanded = value; + + content.ClearTransforms(); + + if (expanded) + content.AutoSizeAxes = Axes.Y; + else + { + content.AutoSizeAxes = Axes.None; + content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + } + + button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint); + } + } + private Color4 buttonActiveColour; protected PlayerSettingsGroup() @@ -82,7 +104,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Position = new Vector2(-15, 0), Icon = FontAwesome.fa_bars, Scale = new Vector2(0.75f), - Action = toggleContentVisibility, + Action = () => Expanded = !Expanded, }, } }, @@ -111,22 +133,5 @@ namespace osu.Game.Screens.Play.PlayerSettings } protected override Container Content => content; - - private void toggleContentVisibility() - { - content.ClearTransforms(); - - expanded = !expanded; - - if (expanded) - content.AutoSizeAxes = Axes.Y; - else - { - content.AutoSizeAxes = Axes.None; - content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - } - - button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint); - } } } From 8b89735e9e5ee84a7d504c0a73b169484812ba0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Feb 2018 14:17:41 +0900 Subject: [PATCH 18/22] Improve xmldoc for DatabaseContextFactory.Get --- osu.Game/Database/DatabaseContextFactory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 2068d6bd8a..712ed2d0cc 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -26,7 +26,8 @@ namespace osu.Game.Database } /// - /// Get a context for read-only usage. + /// Get a context for the current thread for read-only usage. + /// If a is in progress, the existing write-safe context will be returned. /// public OsuDbContext Get() => threadContexts.Value; From 57e61b0b0e49fc6654fbcab291ea81a2841a396c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Feb 2018 14:50:42 +0900 Subject: [PATCH 19/22] Update xmldoc --- osu.Game/Database/ArchiveModelManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 902a42c172..a65593ff82 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -304,8 +304,8 @@ namespace osu.Game.Database /// Create a barebones model from the provided archive. /// Actual expensive population should be done in ; this should just prepare for duplicate checking. /// - /// - /// + /// The archive to create the model for. + /// A model populated with minimal information. protected abstract TModel CreateModel(ArchiveReader archive); /// From ef11ce3dd121a2eabfbffb10c9d655705d087a77 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Feb 2018 17:02:27 +0900 Subject: [PATCH 20/22] Remove Size override from OsuPlayfield --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 17 ++--------------- osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs | 6 +++++- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 17521f8992..7f8cbce78e 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -27,21 +27,8 @@ namespace osu.Game.Rulesets.Osu.UI public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); - public override Vector2 Size - { - get - { - if (Parent == null) - return Vector2.Zero; - - var parentSize = Parent.DrawSize; - var aspectSize = parentSize.X * 0.75f < parentSize.Y ? new Vector2(parentSize.X, parentSize.X * 0.75f) : new Vector2(parentSize.Y * 4f / 3f, parentSize.Y); - - return new Vector2(aspectSize.X / parentSize.X, aspectSize.Y / parentSize.Y) * base.Size; - } - } - - public OsuPlayfield() : base(BASE_SIZE.X) + public OsuPlayfield() + : base(BASE_SIZE.X) { Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 526348062f..07b59c1ef7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -50,7 +50,11 @@ namespace osu.Game.Rulesets.Osu.UI protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay); - protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); + protected override Vector2 GetPlayfieldAspectAdjust() + { + var aspectSize = DrawSize.X * 0.75f < DrawSize.Y ? new Vector2(DrawSize.X, DrawSize.X * 0.75f) : new Vector2(DrawSize.Y * 4f / 3f, DrawSize.Y); + return new Vector2(aspectSize.X / DrawSize.X, aspectSize.Y / DrawSize.Y) * 0.75f; + } protected override CursorContainer CreateCursor() => new GameplayCursor(); } From cd2c9a9de69a7af6d50b6c9c820da7621b0cdb7d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Feb 2018 17:04:18 +0900 Subject: [PATCH 21/22] Adjust xmldoc and rename to GetAspectAdjustedSize --- osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs | 2 +- osu.Game/Rulesets/UI/RulesetContainer.cs | 9 +++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 436d5c1ea6..3c9647117e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.UI return null; } - protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(1, 0.8f); + protected override Vector2 GetAspectAdjustedSize() => new Vector2(1, 0.8f); protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay, this); diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 07b59c1ef7..9cb6a13cb2 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay); - protected override Vector2 GetPlayfieldAspectAdjust() + protected override Vector2 GetAspectAdjustedSize() { var aspectSize = DrawSize.X * 0.75f < DrawSize.Y ? new Vector2(DrawSize.X, DrawSize.X * 0.75f) : new Vector2(DrawSize.Y * 4f / 3f, DrawSize.Y); return new Vector2(aspectSize.X / DrawSize.X, aspectSize.Y / DrawSize.Y) * 0.75f; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 1b9821d698..8342009e80 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - protected override Vector2 GetPlayfieldAspectAdjust() + protected override Vector2 GetAspectAdjustedSize() { const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; const float default_aspect = 16f / 9f; diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 231250e858..8f91c3fcf2 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -324,7 +324,7 @@ namespace osu.Game.Rulesets.UI { base.Update(); - Playfield.Size = AspectAdjust ? GetPlayfieldAspectAdjust() : Vector2.One; + Playfield.Size = AspectAdjust ? GetAspectAdjustedSize() : Vector2.One; } /// @@ -335,10 +335,11 @@ namespace osu.Game.Rulesets.UI protected virtual BeatmapProcessor CreateBeatmapProcessor() => new BeatmapProcessor(); /// - /// In some cases we want to apply changes to the relative size of our contained based on custom conditions. + /// Computes the final size of the in relative coordinate space after all + /// aspect and scale adjustments. /// - /// - protected virtual Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); //a sane default + /// The aspect-adjusted size. + protected virtual Vector2 GetAspectAdjustedSize() => new Vector2(0.75f); // A sane default /// /// Creates a converter to convert Beatmap to a specific mode. From b7be162f28ab16cad692876ed5ebf9442bca2d30 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Feb 2018 17:05:10 +0900 Subject: [PATCH 22/22] Remove AspectAdjust property (override GetAspectAdjustedSize instead) --- osu.Game/Rulesets/UI/RulesetContainer.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 8f91c3fcf2..f4e700a8eb 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -33,11 +33,6 @@ namespace osu.Game.Rulesets.UI /// public abstract class RulesetContainer : Container { - /// - /// Whether to apply adjustments to the child based on our own size. - /// - public bool AspectAdjust = true; - /// /// The selected variant. /// @@ -324,7 +319,7 @@ namespace osu.Game.Rulesets.UI { base.Update(); - Playfield.Size = AspectAdjust ? GetAspectAdjustedSize() : Vector2.One; + Playfield.Size = GetAspectAdjustedSize(); } ///