diff --git a/appveyor.yml b/appveyor.yml index 9cf68803a2..b86082334d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ install: - cmd: git submodule update --init --recursive --depth=5 - cmd: choco install resharper-clt -y - cmd: choco install nvika -y - - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.3/CodeFileSanity.exe + - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.4/CodeFileSanity.exe before_build: - cmd: CodeFileSanity.exe - cmd: nuget restore -verbosity quiet diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index b4dd08eadb..beabeb0a19 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.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.Linq; using osu.Framework.Graphics; @@ -47,16 +48,20 @@ namespace osu.Game.Rulesets.Osu.Mods // fade out immediately after fade in. using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) - { circle.FadeOut(fadeOutDuration); - } break; case DrawableSlider slider: using (slider.BeginAbsoluteSequence(fadeOutStartTime, true)) - { slider.Body.FadeOut(longFadeDuration, Easing.Out); - } + + break; + case DrawableSliderTick sliderTick: + // slider ticks fade out over up to one second + var tickFadeOutDuration = Math.Min(sliderTick.HitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); + + using (sliderTick.BeginAbsoluteSequence(sliderTick.HitObject.StartTime - tickFadeOutDuration, true)) + sliderTick.FadeOut(tickFadeOutDuration); break; case DrawableSpinner spinner: @@ -66,9 +71,7 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.Background.Hide(); using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true)) - { spinner.FadeOut(fadeOutDuration); - } break; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 41d73a745a..baa9eac1a3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking { - private const double anim_duration = 150; + public const double ANIM_DURATION = 150; public bool Tracking { get; set; } @@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdatePreemptState() { this.Animate( - d => d.FadeIn(anim_duration), - d => d.ScaleTo(0.5f).ScaleTo(1f, anim_duration * 4, Easing.OutElasticHalf) + d => d.FadeIn(ANIM_DURATION), + d => d.ScaleTo(0.5f).ScaleTo(1f, ANIM_DURATION * 4, Easing.OutElasticHalf) ); } @@ -64,12 +64,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.Delay(HitObject.TimePreempt).FadeOut(); break; case ArmedState.Miss: - this.FadeOut(anim_duration) - .FadeColour(Color4.Red, anim_duration / 2); + this.FadeOut(ANIM_DURATION) + .FadeColour(Color4.Red, ANIM_DURATION / 2); break; case ArmedState.Hit: - this.FadeOut(anim_duration, Easing.OutQuint) - .ScaleTo(Scale * 1.5f, anim_duration, Easing.Out); + this.FadeOut(ANIM_DURATION, Easing.OutQuint) + .ScaleTo(Scale * 1.5f, ANIM_DURATION, Easing.Out); break; } } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0b49bc8bb9..cade50a9f3 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -26,19 +26,124 @@ namespace osu.Game.Tests.Beatmaps.IO //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed")) { - var osu = loadOsu(host); + try + { + loadOszIntoOsu(loadOsu(host)); + } + finally + { + host.Exit(); + } + } + } - var temp = prepareTempCopy(osz_path); + [Test] + public void TestImportThenDelete() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete")) + { + try + { + var osu = loadOsu(host); - Assert.IsTrue(File.Exists(temp)); + var imported = loadOszIntoOsu(osu); - osu.Dependencies.Get().Import(temp); + deleteBeatmapSet(imported, osu); + } + finally + { + host.Exit(); + } + } + } - ensureLoaded(osu); + [Test] + public void TestImportThenImport() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport")) + { + try + { + var osu = loadOsu(host); - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + var imported = loadOszIntoOsu(osu); + var importedSecondTime = loadOszIntoOsu(osu); - host.Exit(); + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + + var manager = osu.Dependencies.Get(); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestImportThenImportDifferentHash() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImportDifferentHash")) + { + try + { + var osu = loadOsu(host); + var manager = osu.Dependencies.Get(); + + var imported = loadOszIntoOsu(osu); + + //var change = manager.QueryBeatmapSets(_ => true).First(); + imported.Hash += "-changed"; + manager.Update(imported); + + var importedSecondTime = loadOszIntoOsu(osu); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestImportThenDeleteThenImport() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport")) + { + try + { + var osu = loadOsu(host); + + var imported = loadOszIntoOsu(osu); + + deleteBeatmapSet(imported, osu); + + var importedSecondTime = loadOszIntoOsu(osu); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + } + finally + { + host.Exit(); + } } } @@ -50,24 +155,28 @@ namespace osu.Game.Tests.Beatmaps.IO using (HeadlessGameHost host = new CleanRunHeadlessGameHost("host", true)) using (HeadlessGameHost client = new CleanRunHeadlessGameHost("client", true)) { - Assert.IsTrue(host.IsPrimaryInstance); - Assert.IsFalse(client.IsPrimaryInstance); + try + { + Assert.IsTrue(host.IsPrimaryInstance); + Assert.IsFalse(client.IsPrimaryInstance); - var osu = loadOsu(host); + var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp)); - Assert.IsTrue(File.Exists(temp)); + var importer = new BeatmapIPCChannel(client); + if (!importer.ImportAsync(temp).Wait(10000)) + Assert.Fail(@"IPC took too long to send"); - var importer = new BeatmapIPCChannel(client); - if (!importer.ImportAsync(temp).Wait(10000)) - Assert.Fail(@"IPC took too long to send"); + ensureLoaded(osu); - ensureLoaded(osu); - - waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); - - host.Exit(); + waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); + } + finally + { + host.Exit(); + } } } @@ -76,25 +185,49 @@ namespace osu.Game.Tests.Beatmaps.IO { using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen")) { - var osu = loadOsu(host); - - var temp = prepareTempCopy(osz_path); - - Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated"); - - using (File.OpenRead(temp)) - osu.Dependencies.Get().Import(temp); - - ensureLoaded(osu); - - File.Delete(temp); - - Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); - - host.Exit(); + try + { + var osu = loadOsu(host); + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated"); + using (File.OpenRead(temp)) + osu.Dependencies.Get().Import(temp); + ensureLoaded(osu); + File.Delete(temp); + Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); + } + finally + { + host.Exit(); + } } } + private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu) + { + var temp = prepareTempCopy(osz_path); + + Assert.IsTrue(File.Exists(temp)); + + var imported = osu.Dependencies.Get().Import(temp); + + ensureLoaded(osu); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return imported.FirstOrDefault(); + } + + private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) + { + var manager = osu.Dependencies.Get(); + manager.Delete(imported); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); + } + private string prepareTempCopy(string path) { var temp = Path.GetTempFileName(); @@ -105,65 +238,55 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = new OsuGameBase(); Task.Run(() => host.Run(osu)); - waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - return osu; } private void ensureLoaded(OsuGameBase osu, int timeout = 60000) { IEnumerable resultSets = null; - var store = osu.Dependencies.Get(); - waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any(), @"BeatmapSet did not import to the database in allocated time.", timeout); //ensure we were stored to beatmap database backing... Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1)."); - IEnumerable queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0); IEnumerable queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526); //if we don't re-check here, the set will be inserted but the beatmaps won't be present yet. waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout); - waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout); - int countBeatmapSetBeatmaps = 0; int countBeatmaps = 0; - waitForOrAssert(() => - (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == - (countBeatmaps = queryBeatmaps().Count()), + (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == + (countBeatmaps = queryBeatmaps().Count()), $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout); var set = queryBeatmapSets().First(); - foreach (BeatmapInfo b in set.Beatmaps) Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineBeatmapID == b.OnlineBeatmapID)); - Assert.IsTrue(set.Beatmaps.Count > 0); - var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); } private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) { - Action waitAction = () => { while (!result()) Thread.Sleep(200); }; + Action waitAction = () => + { + while (!result()) Thread.Sleep(200); + }; + Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout), failureMessage); } } diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 809de2b8db..8bb0d152f6 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -63,12 +63,10 @@ namespace osu.Game.Tests.Visual var storage = new TestStorage(@"TestCasePlaySongSelect"); // this is by no means clean. should be replacing inside of OsuGameBase somehow. - var context = new OsuDbContext(); + IDatabaseContextFactory factory = new SingletonContextFactory(new OsuDbContext()); - OsuDbContext contextFactory() => context; - - dependencies.Cache(rulesets = new RulesetStore(contextFactory)); - dependencies.Cache(manager = new BeatmapManager(storage, contextFactory, rulesets, null) + dependencies.Cache(rulesets = new RulesetStore(factory)); + dependencies.Cache(manager = new BeatmapManager(storage, factory, rulesets, null) { DefaultBeatmap = defaultBeatmap = game.Beatmap.Default }); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 44289e2400..47773528a6 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -9,31 +9,26 @@ using System.Linq.Expressions; using System.Threading.Tasks; using Ionic.Zip; using Microsoft.EntityFrameworkCore; -using osu.Framework.Audio.Track; using osu.Framework.Extensions; -using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; 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.Graphics.Textures; using osu.Game.IO; using osu.Game.IPC; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using osu.Game.Storyboards; namespace osu.Game.Beatmaps { /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// - public class BeatmapManager + public partial class BeatmapManager { /// /// Fired when a new becomes available in the database. @@ -65,19 +60,7 @@ namespace osu.Game.Beatmaps /// public WorkingBeatmap DefaultBeatmap { private get; set; } - private readonly Storage storage; - - private BeatmapStore createBeatmapStore(Func context) - { - var store = new BeatmapStore(context); - store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); - store.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); - store.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - store.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - return store; - } - - private readonly Func createContext; + private readonly IDatabaseContextFactory contextFactory; private readonly FileStore files; @@ -102,31 +85,19 @@ namespace osu.Game.Beatmaps /// public Func GetStableStorage { private get; set; } - private void refreshImportContext() + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) { - lock (importContextLock) - { - importContext?.Value?.Dispose(); + this.contextFactory = contextFactory; - importContext = new Lazy(() => - { - var c = createContext(); - c.Database.AutoTransactionsEnabled = false; - return c; - }); - } - } + beatmaps = new BeatmapStore(contextFactory); - public BeatmapManager(Storage storage, Func context, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) - { - createContext = context; + beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); + beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); + beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); + beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - refreshImportContext(); + files = new FileStore(contextFactory, storage); - beatmaps = createBeatmapStore(context); - files = new FileStore(context, storage); - - this.storage = files.Storage; this.rulesets = rulesets; this.api = api; @@ -138,10 +109,10 @@ namespace osu.Game.Beatmaps /// /// Import one or more from filesystem . - /// This will post a notification tracking import progress. + /// This will post notifications tracking progress. /// /// One or more beatmap locations on disk. - public void Import(params string[] paths) + public List Import(params string[] paths) { var notification = new ProgressNotification { @@ -153,18 +124,20 @@ namespace osu.Game.Beatmaps PostNotification?.Invoke(notification); + List imported = new List(); + int i = 0; foreach (string path in paths) { if (notification.State == ProgressNotificationState.Cancelled) // user requested abort - return; + return imported; try { notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}"; using (ArchiveReader reader = getReaderFrom(path)) - Import(reader); + imported.Add(Import(reader)); notification.Progress = (float)++i / paths.Length; @@ -186,62 +159,66 @@ namespace osu.Game.Beatmaps { e = e.InnerException ?? e; Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})"); - refreshImportContext(); } } notification.State = ProgressNotificationState.Completed; + return imported; } - private readonly object importContextLock = new object(); - - private Lazy importContext; - /// /// Import a beatmap from an . /// - /// The beatmap to be imported. - public BeatmapSetInfo Import(ArchiveReader archiveReader) + /// The beatmap to be imported. + public BeatmapSetInfo Import(ArchiveReader archive) { - // let's only allow one concurrent import at a time for now. - lock (importContextLock) + using (contextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { - var context = importContext.Value; + // create a new set info (don't yet add to database) + var beatmapSet = createBeatmapSetInfo(archive); - using (var transaction = context.BeginTransaction()) + // 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 local stores so we can isolate and thread safely, and share a context/transaction. - var iFiles = new FileStore(() => context, storage); - var iBeatmaps = createBeatmapStore(() => context); - - BeatmapSetInfo set = importToStorage(iFiles, iBeatmaps, archiveReader); - - if (set.ID == 0) - { - iBeatmaps.Add(set); - context.SaveChanges(); - } - - context.SaveChanges(transaction); - return set; + 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; } } /// /// Import a beatmap from a . /// - /// The beatmap to be imported. - public void Import(BeatmapSetInfo beatmapSetInfo) - { - // If we have an ID then we already exist in the database. - if (beatmapSetInfo.ID != 0) return; - - createBeatmapStore(createContext).Add(beatmapSetInfo); - } + /// The beatmap to be imported. + public void Import(BeatmapSetInfo beatmapSet) => beatmaps.Add(beatmapSet); /// /// Downloads a beatmap. + /// This will post notifications tracking progress. /// /// The to be downloaded. /// Whether the beatmap should be downloaded without video. Defaults to false. @@ -323,6 +300,12 @@ 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 from the manager. /// Is a no-op for already deleted beatmaps. @@ -330,33 +313,29 @@ namespace osu.Game.Beatmaps /// The beatmap set to delete. public void Delete(BeatmapSetInfo beatmapSet) { - lock (importContextLock) + using (var usage = contextFactory.GetForWrite()) { - var context = importContext.Value; + var context = usage.Context; - using (var transaction = context.BeginTransaction()) + 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)) { - 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); - - // create local stores so we can isolate and thread safely, and share a context/transaction. - var iFiles = new FileStore(() => context, storage); - var iBeatmaps = createBeatmapStore(() => context); - - if (iBeatmaps.Delete(beatmapSet)) - { - if (!beatmapSet.Protected) - iFiles.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); - } - - context.ChangeTracker.AutoDetectChangesEnabled = true; - context.SaveChanges(transaction); + 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. + /// public void UndeleteAll() { var deleteMaps = QueryBeatmapSets(bs => bs.DeletePending).ToList(); @@ -388,27 +367,25 @@ namespace osu.Game.Beatmaps 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; - lock (importContextLock) + using (var usage = contextFactory.GetForWrite()) { - var context = importContext.Value; + usage.Context.ChangeTracker.AutoDetectChangesEnabled = false; - using (var transaction = context.BeginTransaction()) - { - context.ChangeTracker.AutoDetectChangesEnabled = false; + if (!beatmaps.Undelete(beatmapSet)) return; - var iFiles = new FileStore(() => context, storage); - var iBeatmaps = createBeatmapStore(() => context); + if (!beatmapSet.Protected) + files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); - undelete(iBeatmaps, iFiles, beatmapSet); - - context.ChangeTracker.AutoDetectChangesEnabled = true; - context.SaveChanges(transaction); - } + usage.Context.ChangeTracker.AutoDetectChangesEnabled = true; } } @@ -424,21 +401,6 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); - /// - /// Returns a to a usable state if it has previously been deleted but not yet purged. - /// Is a no-op for already usable beatmaps. - /// - /// The store to restore beatmaps from. - /// The store to restore beatmap files from. - /// The beatmap to restore. - private void undelete(BeatmapStore beatmaps, FileStore files, BeatmapSetInfo beatmapSet) - { - if (!beatmaps.Undelete(beatmapSet)) return; - - if (!beatmapSet.Protected) - files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); - } - /// /// Retrieve a instance for the provided /// @@ -474,6 +436,12 @@ namespace osu.Game.Beatmaps /// A fresh instance. public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID); + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableBeatmapSets() => beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); + /// /// Perform a lookup query on available s. /// @@ -496,220 +464,8 @@ namespace osu.Game.Beatmaps public IEnumerable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); /// - /// Creates an from a valid storage path. + /// Denotes whether an osu-stable installation is present to perform automated imports from. /// - /// 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(storage.GetStream(path)); - return new LegacyFilesystemReader(path); - } - - /// - /// Import a beamap into our local storage. - /// If the beatmap is already imported, the existing instance will be returned. - /// - /// The store to import beatmap files to. - /// The store to import beatmaps to. - /// The beatmap archive to be read. - /// The imported beatmap, or an existing instance if it is already present. - private BeatmapSetInfo importToStorage(FileStore files, BeatmapStore beatmaps, ArchiveReader reader) - { - // let's make sure there are actually .osu files to import. - string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); - if (string.IsNullOrEmpty(mapName)) - throw new InvalidOperationException("No beatmap files found in the map folder."); - - // for now, concatenate all .osu files in the set to create a unique hash. - MemoryStream hashable = new MemoryStream(); - foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu"))) - using (Stream s = reader.GetStream(file)) - s.CopyTo(hashable); - - var hash = hashable.ComputeSHA2Hash(); - - // check if this beatmap has already been imported and exit early if so. - var beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == hash); - - if (beatmapSet != null) - { - undelete(beatmaps, files, beatmapSet); - - // ensure all files are present and accessible - foreach (var f in beatmapSet.Files) - { - if (!storage.Exists(f.FileInfo.StoragePath)) - using (Stream s = reader.GetStream(f.Filename)) - files.Add(s, false); - } - - // todo: delete any files which shouldn't exist any more. - - return beatmapSet; - } - - 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) - }); - - BeatmapMetadata metadata; - - using (var stream = new StreamReader(reader.GetStream(mapName))) - metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; - - // check if a set already exists with the same online id. - if (metadata.OnlineBeatmapSetID != null) - beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == metadata.OnlineBeatmapSetID); - - if (beatmapSet == null) - beatmapSet = new BeatmapSetInfo - { - OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, - Beatmaps = new List(), - Hash = hash, - Files = fileInfos, - Metadata = metadata - }; - - var mapNames = reader.Filenames.Where(f => f.EndsWith(".osu")); - - foreach (var name in mapNames) - { - using (var raw = reader.GetStream(name)) - using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit - using (var sr = new StreamReader(ms)) - { - raw.CopyTo(ms); - ms.Position = 0; - - var decoder = Decoder.GetDecoder(sr); - Beatmap beatmap = decoder.DecodeBeatmap(sr); - - beatmap.BeatmapInfo.Path = name; - beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); - beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); - - var existing = beatmaps.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.BeatmapInfo.Hash || beatmap.BeatmapInfo.OnlineBeatmapID != null && b.OnlineBeatmapID == beatmap.BeatmapInfo.OnlineBeatmapID); - - if (existing == null) - { - // Exclude beatmap-metadata if it's equal to beatmapset-metadata - if (metadata.Equals(beatmap.Metadata)) - beatmap.BeatmapInfo.Metadata = null; - - RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.Ruleset = ruleset; - beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; - - beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); - } - } - } - - return beatmapSet; - } - - /// - /// Returns a list of all usable s. - /// - /// A list of available . - public List GetAllUsableBeatmapSets() - { - return beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); - } - - protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap - { - private readonly IResourceStore store; - - public BeatmapManagerWorkingBeatmap(IResourceStore store, BeatmapInfo beatmapInfo) - : base(beatmapInfo) - { - this.store = store; - } - - protected override Beatmap GetBeatmap() - { - try - { - using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) - { - Decoder decoder = Decoder.GetDecoder(stream); - return decoder.DecodeBeatmap(stream); - } - } - catch - { - return null; - } - } - - private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; - - protected override Texture GetBackground() - { - if (Metadata?.BackgroundFile == null) - return null; - - try - { - return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); - } - catch - { - return null; - } - } - - protected override Track GetTrack() - { - try - { - var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); - return trackData == null ? null : new TrackBass(trackData); - } - catch - { - return new TrackVirtual(); - } - } - - protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); - - protected override Storyboard GetStoryboard() - { - try - { - using (var beatmap = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) - { - Decoder decoder = Decoder.GetDecoder(beatmap); - - if (BeatmapSetInfo?.StoryboardFile == null) - return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap); - - using (var storyboard = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) - return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, storyboard); - } - } - catch - { - return new Storyboard(); - } - } - } - public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; /// @@ -728,6 +484,10 @@ 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(); @@ -758,5 +518,109 @@ 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. + /// + private string computeBeatmapSetHash(ArchiveReader reader) + { + // for now, concatenate all .osu files in the set to create a unique hash. + MemoryStream hashable = new MemoryStream(); + foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu"))) + using (Stream s = reader.GetStream(file)) + s.CopyTo(hashable); + + return hashable.ComputeSHA2Hash(); + } + + /// + /// Create a from a provided archive. + /// + private BeatmapSetInfo createBeatmapSetInfo(ArchiveReader reader) + { + // let's make sure there are actually .osu files to import. + string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); + if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in the map folder."); + + BeatmapMetadata metadata; + using (var stream = new StreamReader(reader.GetStream(mapName))) + metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; + + return new BeatmapSetInfo + { + OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, + Beatmaps = new List(), + Hash = computeBeatmapSetHash(reader), + Metadata = metadata + }; + } + + /// + /// 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. + /// + private List createBeatmapDifficulties(ArchiveReader reader) + { + var beatmapInfos = new List(); + + foreach (var name in reader.Filenames.Where(f => f.EndsWith(".osu"))) + { + using (var raw = reader.GetStream(name)) + using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit + using (var sr = new StreamReader(ms)) + { + raw.CopyTo(ms); + ms.Position = 0; + + var decoder = Decoder.GetDecoder(sr); + Beatmap beatmap = decoder.DecodeBeatmap(sr); + + beatmap.BeatmapInfo.Path = name; + beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); + beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + + RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.Ruleset = ruleset; + beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; + + beatmapInfos.Add(beatmap.BeatmapInfo); + } + } + + return beatmapInfos; + } } } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs new file mode 100644 index 0000000000..14a4028b44 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -0,0 +1,98 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Game.Beatmaps.Formats; +using osu.Game.Graphics.Textures; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps +{ + public partial class BeatmapManager + { + protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap + { + private readonly IResourceStore store; + + public BeatmapManagerWorkingBeatmap(IResourceStore store, BeatmapInfo beatmapInfo) + : base(beatmapInfo) + { + this.store = store; + } + + protected override Beatmap GetBeatmap() + { + try + { + using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + { + Decoder decoder = Decoder.GetDecoder(stream); + return decoder.DecodeBeatmap(stream); + } + } + catch + { + return null; + } + } + + private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; + + protected override Texture GetBackground() + { + if (Metadata?.BackgroundFile == null) + return null; + + try + { + return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); + } + catch + { + return null; + } + } + + protected override Track GetTrack() + { + try + { + var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); + return trackData == null ? null : new TrackBass(trackData); + } + catch + { + return new TrackVirtual(); + } + } + + protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); + + protected override Storyboard GetStoryboard() + { + try + { + using (var beatmap = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + { + Decoder decoder = Decoder.GetDecoder(beatmap); + + if (BeatmapSetInfo?.StoryboardFile == null) + return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap); + + using (var storyboard = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) + return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, storyboard); + } + } + catch + { + return new Storyboard(); + } + } + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index df71c5c0d0..29373c0715 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using osu.Game.Database; @@ -19,7 +20,7 @@ namespace osu.Game.Beatmaps public event Action BeatmapHidden; public event Action BeatmapRestored; - public BeatmapStore(Func factory) + public BeatmapStore(IDatabaseContextFactory factory) : base(factory) { } @@ -30,22 +31,38 @@ namespace osu.Game.Beatmaps /// The beatmap to add. public void Add(BeatmapSetInfo beatmapSet) { - var context = GetContext(); - - foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null)) + using (var usage = ContextFactory.GetForWrite()) { - // 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); - } + var context = usage.Context; - context.BeatmapSetInfo.Attach(beatmapSet); - context.SaveChanges(); + 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); } @@ -57,13 +74,14 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Delete(BeatmapSetInfo beatmapSet) { - var context = GetContext(); + using (ContextFactory.GetForWrite()) + { + Refresh(ref beatmapSet, BeatmapSets); - Refresh(ref beatmapSet, BeatmapSets); + if (beatmapSet.DeletePending) return false; - if (beatmapSet.DeletePending) return false; - beatmapSet.DeletePending = true; - context.SaveChanges(); + beatmapSet.DeletePending = true; + } BeatmapSetRemoved?.Invoke(beatmapSet); return true; @@ -76,13 +94,14 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Undelete(BeatmapSetInfo beatmapSet) { - var context = GetContext(); + using (ContextFactory.GetForWrite()) + { + Refresh(ref beatmapSet, BeatmapSets); - Refresh(ref beatmapSet, BeatmapSets); + if (!beatmapSet.DeletePending) return false; - if (!beatmapSet.DeletePending) return false; - beatmapSet.DeletePending = false; - context.SaveChanges(); + beatmapSet.DeletePending = false; + } BeatmapSetAdded?.Invoke(beatmapSet); return true; @@ -95,15 +114,17 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Hide(BeatmapInfo beatmap) { - var context = GetContext(); + using (ContextFactory.GetForWrite()) + { + Refresh(ref beatmap, Beatmaps); - Refresh(ref beatmap, Beatmaps); + if (beatmap.Hidden) return false; - if (beatmap.Hidden) return false; - beatmap.Hidden = true; - context.SaveChanges(); + beatmap.Hidden = true; + + BeatmapHidden?.Invoke(beatmap); + } - BeatmapHidden?.Invoke(beatmap); return true; } @@ -114,47 +135,55 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Restore(BeatmapInfo beatmap) { - var context = GetContext(); + using (ContextFactory.GetForWrite()) + { + Refresh(ref beatmap, Beatmaps); - Refresh(ref beatmap, Beatmaps); + if (!beatmap.Hidden) return false; - if (!beatmap.Hidden) return false; - beatmap.Hidden = false; - context.SaveChanges(); + beatmap.Hidden = false; + } BeatmapRestored?.Invoke(beatmap); return true; } - public override void Cleanup() + public override void Cleanup() => Cleanup(_ => true); + + public void Cleanup(Expression> query) { - var context = GetContext(); + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; - var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected) - .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(); - // 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))); + 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(purgeable.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))); - // cascades down to beatmaps. - context.BeatmapSetInfo.RemoveRange(purgeable); - context.SaveChanges(); + // 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); + } } - public IQueryable BeatmapSets => GetContext().BeatmapSetInfo + 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 => GetContext().BeatmapInfo + 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) diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index 9b18151c84..7b66002a79 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -12,8 +12,8 @@ namespace osu.Game.Configuration { public event Action SettingChanged; - public SettingsStore(Func createContext) - : base(createContext) + public SettingsStore(DatabaseContextFactory contextFactory) + : base(contextFactory) { } @@ -24,19 +24,16 @@ namespace osu.Game.Configuration /// An optional variant. /// public List Query(int? rulesetId = null, int? variant = null) => - GetContext().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); public void Update(DatabasedSetting setting) { - var context = GetContext(); - - var newValue = setting.Value; - - Refresh(ref setting); - - setting.Value = newValue; - - context.SaveChanges(); + using (ContextFactory.GetForWrite()) + { + var newValue = setting.Value; + Refresh(ref setting); + setting.Value = newValue; + } SettingChanged?.Invoke(); } diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index ec9967e097..cf46b66422 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -1,10 +1,8 @@ // 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.Threading; using Microsoft.EntityFrameworkCore; using osu.Framework.Platform; @@ -17,9 +15,7 @@ namespace osu.Game.Database /// /// Create a new instance (separate from the shared context via for performing isolated operations. /// - protected readonly Func CreateContext; - - private readonly ThreadLocal queryContext; + protected readonly IDatabaseContextFactory ContextFactory; /// /// Refresh an instance potentially from a different thread with a local context-tracked instance. @@ -29,33 +25,24 @@ namespace osu.Game.Database /// A valid EF-stored type. protected virtual void Refresh(ref T obj, IEnumerable lookupSource = null) where T : class, IHasPrimaryKey { - var context = GetContext(); - - if (context.Entry(obj).State != EntityState.Detached) return; - - var id = obj.ID; - var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); - if (foundObject != null) + using (var usage = ContextFactory.GetForWrite()) { - obj = foundObject; - context.Entry(obj).Reload(); + var context = usage.Context; + + if (context.Entry(obj).State != EntityState.Detached) return; + + var id = obj.ID; + var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); + if (foundObject != null) + obj = foundObject; + else + context.Add(obj); } - else - context.Add(obj); } - /// - /// Retrieve a shared context for performing lookups (or write operations on the update thread, for now). - /// - protected OsuDbContext GetContext() => queryContext.Value; - - protected DatabaseBackedStore(Func createContext, Storage storage = null) + protected DatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) { - CreateContext = createContext; - - // todo: while this seems to work quite well, we need to consider that contexts could enter a state where they are never cleaned up. - queryContext = new ThreadLocal(CreateContext); - + ContextFactory = contextFactory; Storage = storage; } diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index b1917d92c4..2068d6bd8a 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -1,27 +1,93 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Threading; using osu.Framework.Platform; namespace osu.Game.Database { - public class DatabaseContextFactory + public class DatabaseContextFactory : IDatabaseContextFactory { private readonly GameHost host; private const string database_name = @"client"; + private ThreadLocal threadContexts; + + private readonly object writeLock = new object(); + + private bool currentWriteDidWrite; + private volatile int currentWriteUsages; + public DatabaseContextFactory(GameHost host) { this.host = host; + recycleThreadContexts(); } - public OsuDbContext GetContext() => new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)); + /// + /// Get a context for read-only usage. + /// + public OsuDbContext Get() => threadContexts.Value; + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + public DatabaseWriteUsage GetForWrite() + { + Monitor.Enter(writeLock); + + Interlocked.Increment(ref currentWriteUsages); + + return new DatabaseWriteUsage(threadContexts.Value, usageCompleted); + } + + private void usageCompleted(DatabaseWriteUsage usage) + { + int usages = Interlocked.Decrement(ref currentWriteUsages); + + try + { + currentWriteDidWrite |= usage.PerformedWrite; + + if (usages > 0) return; + + if (currentWriteDidWrite) + { + // explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged). + usage.Context.Dispose(); + + currentWriteDidWrite = false; + + // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. + recycleThreadContexts(); + } + } + finally + { + Monitor.Exit(writeLock); + } + } + + private void recycleThreadContexts() => threadContexts = new ThreadLocal(CreateContext); + + protected virtual OsuDbContext CreateContext() + { + var ctx = new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)); + ctx.Database.AutoTransactionsEnabled = false; + + return ctx; + } public void ResetDatabase() { - // todo: we probably want to make sure there are no active contexts before performing this operation. - host.Storage.DeleteDatabase(database_name); + lock (writeLock) + { + recycleThreadContexts(); + host.Storage.DeleteDatabase(database_name); + } } } } diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs new file mode 100644 index 0000000000..52dd0ee268 --- /dev/null +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using Microsoft.EntityFrameworkCore.Storage; + +namespace osu.Game.Database +{ + public class DatabaseWriteUsage : IDisposable + { + public readonly OsuDbContext Context; + private readonly IDbContextTransaction transaction; + private readonly Action usageCompleted; + + public DatabaseWriteUsage(OsuDbContext context, Action onCompleted) + { + Context = context; + transaction = Context.BeginTransaction(); + usageCompleted = onCompleted; + } + + public bool PerformedWrite { get; private set; } + + private bool isDisposed; + + protected void Dispose(bool disposing) + { + if (isDisposed) return; + isDisposed = true; + + PerformedWrite |= Context.SaveChanges(transaction) > 0; + usageCompleted?.Invoke(this); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~DatabaseWriteUsage() + { + Dispose(false); + } + } +} diff --git a/osu.Game/Database/IDatabaseContextFactory.cs b/osu.Game/Database/IDatabaseContextFactory.cs new file mode 100644 index 0000000000..bc1bc0349c --- /dev/null +++ b/osu.Game/Database/IDatabaseContextFactory.cs @@ -0,0 +1,20 @@ +// 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 IDatabaseContextFactory + { + /// + /// Get a context for read-only usage. + /// + OsuDbContext Get(); + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + DatabaseWriteUsage GetForWrite(); + } +} diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 0fa1f238a9..e83b30595e 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -111,7 +111,7 @@ namespace osu.Game.Database public int SaveChanges(IDbContextTransaction transaction = null) { var ret = base.SaveChanges(); - transaction?.Commit(); + if (ret > 0) transaction?.Commit(); return ret; } @@ -186,8 +186,6 @@ namespace osu.Game.Database public void Migrate() { - migrateFromSqliteNet(); - try { Database.Migrate(); @@ -197,86 +195,6 @@ namespace osu.Game.Database throw new MigrationFailedException(e); } } - - private void migrateFromSqliteNet() - { - try - { - // will fail if the database isn't in a sane EF-migrated state. - Database.ExecuteSqlCommand("SELECT MetadataID FROM BeatmapSetInfo LIMIT 1"); - } - catch - { - try - { - Database.ExecuteSqlCommand("DROP TABLE IF EXISTS __EFMigrationsHistory"); - - // will fail (intentionally) if we don't have sqlite-net data present. - Database.ExecuteSqlCommand("SELECT OnlineBeatmapSetId FROM BeatmapMetadata LIMIT 1"); - - try - { - Logger.Log("Performing migration from sqlite-net to EF...", LoggingTarget.Database, Framework.Logging.LogLevel.Important); - - // we are good to perform messy migration of data!. - Database.ExecuteSqlCommand("ALTER TABLE BeatmapDifficulty RENAME TO BeatmapDifficulty_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapMetadata RENAME TO BeatmapMetadata_Old"); - Database.ExecuteSqlCommand("ALTER TABLE FileInfo RENAME TO FileInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE KeyBinding RENAME TO KeyBinding_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetInfo RENAME TO BeatmapSetInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapInfo RENAME TO BeatmapInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetFileInfo RENAME TO BeatmapSetFileInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE RulesetInfo RENAME TO RulesetInfo_Old"); - - Database.ExecuteSqlCommand("DROP TABLE StoreVersion"); - - // perform EF migrations to create sane table structure. - Database.Migrate(); - - // copy data table by table to new structure, dropping old tables as we go. - Database.ExecuteSqlCommand("INSERT INTO FileInfo SELECT * FROM FileInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE FileInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO KeyBinding SELECT ID, [Action], Keys, RulesetID, Variant FROM KeyBinding_Old"); - Database.ExecuteSqlCommand("DROP TABLE KeyBinding_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapMetadata SELECT ID, Artist, ArtistUnicode, AudioFile, Author, BackgroundFile, PreviewTime, Source, Tags, Title, TitleUnicode FROM BeatmapMetadata_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapMetadata_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapDifficulty SELECT `ID`, `ApproachRate`, `CircleSize`, `DrainRate`, `OverallDifficulty`, `SliderMultiplier`, `SliderTickRate` FROM BeatmapDifficulty_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapDifficulty_Old"); - - Database.ExecuteSqlCommand("INSERT INTO BeatmapSetInfo SELECT ID, DeletePending, Hash, BeatmapMetadataID, OnlineBeatmapSetID, Protected FROM BeatmapSetInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapSetInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO BeatmapSetFileInfo SELECT ID, BeatmapSetInfoID, FileInfoID, Filename FROM BeatmapSetFileInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapSetFileInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO RulesetInfo SELECT ID, Available, InstantiationInfo, Name FROM RulesetInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE RulesetInfo_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapInfo SELECT ID, AudioLeadIn, BaseDifficultyID, BeatDivisor, BeatmapSetInfoID, Countdown, DistanceSpacing, GridSize, Hash, IFNULL(Hidden, 0), LetterboxInBreaks, MD5Hash, NULLIF(BeatmapMetadataID, 0), NULLIF(OnlineBeatmapID, 0), Path, RulesetID, SpecialStyle, StackLeniency, StarDifficulty, StoredBookmarks, TimelineZoom, Version, WidescreenStoryboard FROM BeatmapInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapInfo_Old"); - - Logger.Log("Migration complete!", LoggingTarget.Database, Framework.Logging.LogLevel.Important); - } - catch (Exception e) - { - throw new MigrationFailedException(e); - } - } - catch (MigrationFailedException) - { - throw; - } - catch - { - } - } - } } public class MigrationFailedException : Exception diff --git a/osu.Game/Database/SingletonContextFactory.cs b/osu.Game/Database/SingletonContextFactory.cs new file mode 100644 index 0000000000..067e4fd8eb --- /dev/null +++ b/osu.Game/Database/SingletonContextFactory.cs @@ -0,0 +1,19 @@ +// 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 class SingletonContextFactory : IDatabaseContextFactory + { + private readonly OsuDbContext context; + + public SingletonContextFactory(OsuDbContext context) + { + this.context = context; + } + + public OsuDbContext Get() => context; + + public DatabaseWriteUsage GetForWrite() => new DatabaseWriteUsage(context, null); + } +} diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 31c608a5f4..ab81ba4851 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -21,86 +21,94 @@ namespace osu.Game.IO public new Storage Storage => base.Storage; - public FileStore(Func createContext, Storage storage) : base(createContext, storage.GetStorageForDirectory(@"files")) + public FileStore(IDatabaseContextFactory contextFactory, Storage storage) : base(contextFactory, storage.GetStorageForDirectory(@"files")) { Store = new StorageBackedResourceStore(Storage); } public FileInfo Add(Stream data, bool reference = true) { - var context = GetContext(); - - string hash = data.ComputeSHA2Hash(); - - var existing = context.FileInfo.FirstOrDefault(f => f.Hash == hash); - - var info = existing ?? new FileInfo { Hash = hash }; - - string path = info.StoragePath; - - // we may be re-adding a file to fix missing store entries. - if (!Storage.Exists(path)) + using (var usage = ContextFactory.GetForWrite()) { - data.Seek(0, SeekOrigin.Begin); + string hash = data.ComputeSHA2Hash(); - using (var output = Storage.GetStream(path, FileAccess.Write)) - data.CopyTo(output); + var existing = usage.Context.FileInfo.FirstOrDefault(f => f.Hash == hash); - data.Seek(0, SeekOrigin.Begin); + var info = existing ?? new FileInfo { Hash = hash }; + + string path = info.StoragePath; + + // we may be re-adding a file to fix missing store entries. + if (!Storage.Exists(path)) + { + data.Seek(0, SeekOrigin.Begin); + + using (var output = Storage.GetStream(path, FileAccess.Write)) + data.CopyTo(output); + + data.Seek(0, SeekOrigin.Begin); + } + + if (reference || existing == null) + Reference(info); + + return info; } - - if (reference || existing == null) - Reference(info); - - return info; } - public void Reference(params FileInfo[] files) => reference(GetContext(), files); - - private void reference(OsuDbContext context, FileInfo[] files) + public void Reference(params FileInfo[] files) { - foreach (var f in files.GroupBy(f => f.ID)) - { - var refetch = context.Find(f.First().ID) ?? f.First(); - refetch.ReferenceCount += f.Count(); - context.FileInfo.Update(refetch); - } + if (files.Length == 0) return; - context.SaveChanges(); + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + foreach (var f in files.GroupBy(f => f.ID)) + { + var refetch = context.Find(f.First().ID) ?? f.First(); + refetch.ReferenceCount += f.Count(); + context.FileInfo.Update(refetch); + } + } } - public void Dereference(params FileInfo[] files) => dereference(GetContext(), files); - - private void dereference(OsuDbContext context, FileInfo[] files) + public void Dereference(params FileInfo[] files) { - foreach (var f in files.GroupBy(f => f.ID)) - { - var refetch = context.FileInfo.Find(f.Key); - refetch.ReferenceCount -= f.Count(); - context.FileInfo.Update(refetch); - } + if (files.Length == 0) return; - context.SaveChanges(); + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + foreach (var f in files.GroupBy(f => f.ID)) + { + var refetch = context.FileInfo.Find(f.Key); + refetch.ReferenceCount -= f.Count(); + context.FileInfo.Update(refetch); + } + } } public override void Cleanup() { - var context = GetContext(); - - foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) + using (var usage = ContextFactory.GetForWrite()) { - try + var context = usage.Context; + + foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) { - Storage.Delete(f.StoragePath); - context.FileInfo.Remove(f); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete beatmap {f}"); + try + { + Storage.Delete(f.StoragePath); + context.FileInfo.Remove(f); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete beatmap {f}"); + } } } - - context.SaveChanges(); } } } diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 92159ab491..33cb0911a8 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -16,14 +16,17 @@ namespace osu.Game.Input { public event Action KeyBindingChanged; - public KeyBindingStore(Func createContext, RulesetStore rulesets, Storage storage = null) - : base(createContext, storage) + public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) + : base(contextFactory, storage) { - foreach (var info in rulesets.AvailableRulesets) + using (ContextFactory.GetForWrite()) { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + foreach (var info in rulesets.AvailableRulesets) + { + var ruleset = info.CreateInstance(); + foreach (var variant in ruleset.AvailableVariants) + insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + } } } @@ -31,9 +34,7 @@ namespace osu.Game.Input private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { - var context = GetContext(); - - using (var transaction = context.BeginTransaction()) + using (var usage = ContextFactory.GetForWrite()) { // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) @@ -46,7 +47,7 @@ namespace osu.Game.Input foreach (var insertable in group.Skip(count).Take(aimCount - count)) // insert any defaults which are missing. - context.DatabasedKeyBinding.Add(new DatabasedKeyBinding + usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding { KeyCombination = insertable.KeyCombination, Action = insertable.Action, @@ -54,8 +55,6 @@ namespace osu.Game.Input Variant = variant }); } - - context.SaveChanges(transaction); } } @@ -66,19 +65,20 @@ namespace osu.Game.Input /// An optional variant. /// public List Query(int? rulesetId = null, int? variant = null) => - GetContext().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); public void Update(KeyBinding keyBinding) { - var dbKeyBinding = (DatabasedKeyBinding)keyBinding; + using (ContextFactory.GetForWrite()) + { + var dbKeyBinding = (DatabasedKeyBinding)keyBinding; + Refresh(ref dbKeyBinding); - var context = GetContext(); + if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) + return; - Refresh(ref dbKeyBinding); - - dbKeyBinding.KeyCombination = keyBinding.KeyCombination; - - context.SaveChanges(); + dbKeyBinding.KeyCombination = keyBinding.KeyCombination; + } KeyBindingChanged?.Invoke(); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 937b204c81..505577416d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -106,12 +106,12 @@ namespace osu.Game Token = LocalConfig.Get(OsuSetting.Token) }); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory.GetContext)); - dependencies.Cache(FileStore = new FileStore(contextFactory.GetContext, Host.Storage)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory.GetContext, RulesetStore, API, Host)); - dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory.GetContext, Host, BeatmapManager, RulesetStore)); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory.GetContext, RulesetStore)); - dependencies.Cache(SettingsStore = new SettingsStore(contextFactory.GetContext)); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); + dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Host)); + dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore)); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(new OsuColour()); //this completely overrides the framework default. will need to change once we make a proper FontStore. @@ -179,8 +179,8 @@ namespace osu.Game { try { - using (var context = contextFactory.GetContext()) - context.Migrate(); + using (var db = contextFactory.GetForWrite()) + db.Context.Migrate(); } catch (MigrationFailedException e) { @@ -191,8 +191,8 @@ namespace osu.Game contextFactory.ResetDatabase(); Logger.Log("Database purged successfully.", LoggingTarget.Database, LogLevel.Important); - using (var context = contextFactory.GetContext()) - context.Migrate(); + using (var db = contextFactory.GetForWrite()) + db.Context.Migrate(); } } @@ -218,7 +218,7 @@ namespace osu.Game CursorOverrideContainer.Child = globalBinding = new GlobalActionContainer(this) { RelativeSizeAxes = Axes.Both, - Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both } + Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both } }; base.Content.Add(new DrawSizePreservingFillContainer { Child = CursorOverrideContainer }); diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 01e3b6848f..92fbf25f04 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets loadRulesetFromFile(file); } - public RulesetStore(Func factory) + public RulesetStore(IDatabaseContextFactory factory) : base(factory) { AddMissingRulesets(); @@ -56,47 +56,50 @@ namespace osu.Game.Rulesets protected void AddMissingRulesets() { - var context = GetContext(); - - var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); - - //add all legacy modes in correct order - foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID)) + using (var usage = ContextFactory.GetForWrite()) { - if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null) - context.RulesetInfo.Add(r.RulesetInfo); - } + var context = usage.Context; - context.SaveChanges(); + var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); - //add any other modes - foreach (var r in instances.Where(r => r.LegacyID < 0)) - if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) - context.RulesetInfo.Add(r.RulesetInfo); - - context.SaveChanges(); - - //perform a consistency check - foreach (var r in context.RulesetInfo) - { - try + //add all legacy modes in correct order + foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID)) { - var instance = r.CreateInstance(); - - r.Name = instance.Description; - r.ShortName = instance.ShortName; - - r.Available = true; + if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null) + context.RulesetInfo.Add(r.RulesetInfo); } - catch + + context.SaveChanges(); + + //add any other modes + foreach (var r in instances.Where(r => r.LegacyID < 0)) + if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) + context.RulesetInfo.Add(r.RulesetInfo); + + context.SaveChanges(); + + //perform a consistency check + foreach (var r in context.RulesetInfo) { - r.Available = false; + try + { + var instance = r.CreateInstance(); + + r.Name = instance.Description; + r.ShortName = instance.ShortName; + + r.Available = true; + } + catch + { + r.Available = false; + } } + + context.SaveChanges(); + + AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList(); } - - context.SaveChanges(); - - AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList(); } private static void loadRulesetFromFile(string file) diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index d21ca79736..8bde2747a2 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.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; using System.Collections.Generic; using System.IO; using osu.Framework.Platform; @@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Scoring // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ScoreIPCChannel ipc; - public ScoreStore(Storage storage, Func factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory) + public ScoreStore(Storage storage, DatabaseContextFactory factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory) { this.storage = storage; this.beatmaps = beatmaps; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index d8cfd79e12..5204b7d787 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -25,10 +25,10 @@ namespace osu.Game.Screens.Select.Carousel { public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu { - private Action deleteRequested; private Action restoreHiddenRequested; private Action viewDetails; + private DialogOverlay dialogOverlay; private readonly BeatmapSetInfo beatmapSet; public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) @@ -38,13 +38,13 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader(true)] - private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay) + private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay, DialogOverlay overlay) { if (localisation == null) throw new ArgumentNullException(nameof(localisation)); restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore); - deleteRequested = manager.Delete; + dialogOverlay = overlay; if (beatmapOverlay != null) viewDetails = beatmapOverlay.ShowBeatmapSet; @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested?.Invoke(beatmapSet))); - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => deleteRequested?.Invoke(beatmapSet))); + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new BeatmapDeleteDialog(beatmapSet)))); return items.ToArray(); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6542160b97..02801eb81f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -248,6 +248,7 @@ + @@ -274,7 +275,10 @@ + + +