diff --git a/osu-framework b/osu-framework index 943410e228..b64322a56d 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 943410e228f704252a803cee75532434c6b5be72 +Subproject commit b64322a56d3d55e8e5d1e6c3328024923cecd4d3 diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs index a9b9f796f1..7533a1a1e6 100644 --- a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs +++ b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using OpenTK; using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps.IO; using osu.Game.Database; using osu.Game.Modes; using osu.Game.Modes.Objects; @@ -72,7 +73,7 @@ namespace osu.Desktop.VisualTests.Tests decoder.Process(b); - beatmap = new WorkingBeatmap(b); + beatmap = new TestWorkingBeatmap(b); } Add(new Box @@ -90,5 +91,16 @@ namespace osu.Desktop.VisualTests.Tests Beatmap = beatmap }); } + + class TestWorkingBeatmap : WorkingBeatmap + { + public TestWorkingBeatmap(Beatmap beatmap) + : base(beatmap.BeatmapInfo, beatmap.BeatmapInfo.BeatmapSet) + { + Beatmap = beatmap; + } + + protected override ArchiveReader GetReader() => null; + } } } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index ce702e5679..fe0abd6d87 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(set.Beatmaps.Count > 0); - var beatmap = osu.Dependencies.Get().GetBeatmap(set.Beatmaps.First(b => b.Mode == PlayMode.Osu)); + var beatmap = osu.Dependencies.Get().GetWorkingBeatmap(set.Beatmaps.First(b => b.Mode == PlayMode.Osu))?.Beatmap; Assert.IsTrue(beatmap.HitObjects.Count > 0); } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index c6bc4eeeb8..9393c2b0e9 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -11,16 +11,22 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { - public class WorkingBeatmap : IDisposable + public abstract class WorkingBeatmap : IDisposable { public readonly BeatmapInfo BeatmapInfo; public readonly BeatmapSetInfo BeatmapSetInfo; - private readonly BeatmapDatabase database; public readonly bool WithStoryboard; - private ArchiveReader getReader() => database?.GetReader(BeatmapSetInfo); + protected abstract ArchiveReader GetReader(); + + protected WorkingBeatmap(BeatmapInfo beatmapInfo, BeatmapSetInfo beatmapSetInfo, bool withStoryboard = false) + { + BeatmapInfo = beatmapInfo; + BeatmapSetInfo = beatmapSetInfo; + WithStoryboard = withStoryboard; + } private Texture background; private object backgroundLock = new object(); @@ -36,7 +42,7 @@ namespace osu.Game.Beatmaps try { - using (var reader = getReader()) + using (var reader = GetReader()) background = new TextureStore(new RawTextureLoaderStore(reader), false).Get(BeatmapInfo.Metadata.BackgroundFile); } catch { } @@ -59,7 +65,7 @@ namespace osu.Game.Beatmaps try { - using (var reader = getReader()) + using (var reader = GetReader()) { BeatmapDecoder decoder; using (var stream = new StreamReader(reader.GetStream(BeatmapInfo.Path))) @@ -95,7 +101,7 @@ namespace osu.Game.Beatmaps try { //store a reference to the reader as we may continue accessing the stream in the background. - trackReader = getReader(); + trackReader = GetReader(); var trackData = trackReader?.GetStream(BeatmapInfo.Metadata.AudioFile); if (trackData != null) track = new TrackBass(trackData); @@ -110,21 +116,6 @@ namespace osu.Game.Beatmaps public bool TrackLoaded => track != null; - public WorkingBeatmap(Beatmap beatmap) - { - this.beatmap = beatmap; - BeatmapInfo = beatmap.BeatmapInfo; - BeatmapSetInfo = beatmap.BeatmapInfo.BeatmapSet; - } - - public WorkingBeatmap(BeatmapInfo beatmapInfo, BeatmapSetInfo beatmapSetInfo, BeatmapDatabase database, bool withStoryboard = false) - { - BeatmapInfo = beatmapInfo; - BeatmapSetInfo = beatmapSetInfo; - this.database = database; - this.WithStoryboard = withStoryboard; - } - private bool isDisposed; protected virtual void Dispose(bool disposing) diff --git a/osu.Game/Database/BeatmapDatabase.cs b/osu.Game/Database/BeatmapDatabase.cs index 253b83aa38..f962146717 100644 --- a/osu.Game/Database/BeatmapDatabase.cs +++ b/osu.Game/Database/BeatmapDatabase.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Linq.Expressions; using System.Security.Cryptography; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; @@ -22,6 +23,7 @@ namespace osu.Game.Database private SQLiteConnection connection { get; set; } private Storage storage; public event Action BeatmapSetAdded; + public event Action BeatmapSetRemoved; private BeatmapImporter ipc; @@ -37,16 +39,37 @@ namespace osu.Game.Database try { connection = prepareConnection(); + deletePending(); } - catch + catch (Exception e) { - Console.WriteLine(@"Failed to initialise the beatmap database! Trying again with a clean database..."); + Logger.Error(e, @"Failed to initialise the beatmap database! Trying again with a clean database..."); storage.DeleteDatabase(@"beatmaps"); connection = prepareConnection(); } } } + private void deletePending() + { + foreach (var b in Query().Where(b => b.DeletePending)) + { + try + { + storage.Delete(b.Path); + connection.Delete(b); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete beatmap {b.ToString()}"); + } + } + + //this is required because sqlite migrations don't work, initially inserting nulls into this field. + //see https://github.com/praeclarum/sqlite-net/issues/326 + connection.Query("UPDATE BeatmapSetInfo SET DeletePending = 0 WHERE DeletePending IS NULL"); + } + private SQLiteConnection prepareConnection() { var conn = storage.GetDatabase(@"beatmaps"); @@ -157,6 +180,14 @@ namespace osu.Game.Database connection.Commit(); } + public void Delete(BeatmapSetInfo beatmapSet) + { + beatmapSet.DeletePending = true; + Update(beatmapSet, false); + + BeatmapSetRemoved?.Invoke(beatmapSet); + } + public ArchiveReader GetReader(BeatmapSetInfo beatmapSet) { if (string.IsNullOrEmpty(beatmapSet.Path)) @@ -183,19 +214,13 @@ namespace osu.Game.Database if (beatmapInfo.Metadata == null) beatmapInfo.Metadata = beatmapSetInfo.Metadata; - var working = new WorkingBeatmap(beatmapInfo, beatmapSetInfo, this, withStoryboard); + WorkingBeatmap working = new DatabaseWorkingBeatmap(this, beatmapInfo, beatmapSetInfo, withStoryboard); previous?.TransferTo(working); return working; } - public Beatmap GetBeatmap(BeatmapInfo beatmapInfo) - { - using (WorkingBeatmap data = GetWorkingBeatmap(beatmapInfo)) - return data.Beatmap; - } - public TableQuery Query() where T : class { return connection.Table(); @@ -237,5 +262,20 @@ namespace osu.Game.Database else connection.Update(record); } + + public bool Exists(BeatmapSetInfo beatmapSet) => storage.Exists(beatmapSet.Path); + + private class DatabaseWorkingBeatmap : WorkingBeatmap + { + private readonly BeatmapDatabase database; + + public DatabaseWorkingBeatmap(BeatmapDatabase database, BeatmapInfo beatmapInfo, BeatmapSetInfo beatmapSetInfo, bool withStoryboard = false) + : base(beatmapInfo, beatmapSetInfo, withStoryboard) + { + this.database = database; + } + + protected override ArchiveReader GetReader() => database?.GetReader(BeatmapSetInfo); + } } } diff --git a/osu.Game/Database/BeatmapInfo.cs b/osu.Game/Database/BeatmapInfo.cs index dd0fffe855..9622b73108 100644 --- a/osu.Game/Database/BeatmapInfo.cs +++ b/osu.Game/Database/BeatmapInfo.cs @@ -87,8 +87,7 @@ namespace osu.Game.Database internal void ComputeDifficulty(BeatmapDatabase database) { - WorkingBeatmap wb = new WorkingBeatmap(this, BeatmapSet, database); - StarDifficulty = (float)Ruleset.GetRuleset(Mode).CreateDifficultyCalculator(wb.Beatmap).GetDifficulty(); + StarDifficulty = (float)Ruleset.GetRuleset(Mode).CreateDifficultyCalculator(database.GetWorkingBeatmap(this).Beatmap).GetDifficulty(); } public bool Equals(BeatmapInfo other) diff --git a/osu.Game/Database/BeatmapSetInfo.cs b/osu.Game/Database/BeatmapSetInfo.cs index 88d35d20a8..e4e9c35963 100644 --- a/osu.Game/Database/BeatmapSetInfo.cs +++ b/osu.Game/Database/BeatmapSetInfo.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.ComponentModel; using SQLite.Net.Attributes; using SQLiteNetExtensions.Attributes; @@ -23,6 +24,8 @@ namespace osu.Game.Database [OneToMany(CascadeOperations = CascadeOperation.All)] public List Beatmaps { get; set; } + public bool DeletePending { get; set; } + public string Hash { get; set; } public string Path { get; set; } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 1f087ac4a6..aa6b3c6247 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -254,14 +254,16 @@ namespace osu.Game.Overlays { progress.IsEnabled = (beatmapSource.Value != null); if (beatmapSource.Value == current) return; - bool audioEquals = current?.BeatmapInfo.AudioEquals(beatmapSource.Value.BeatmapInfo) ?? false; + bool audioEquals = current?.BeatmapInfo?.AudioEquals(beatmapSource?.Value?.BeatmapInfo) ?? false; current = beatmapSource.Value; updateDisplay(current, audioEquals ? TransformDirection.None : TransformDirection.Next); - appendToHistory(current.BeatmapInfo); + appendToHistory(current?.BeatmapInfo); } private void appendToHistory(BeatmapInfo beatmap) { + if (beatmap == null) return; + if (playHistoryIndex >= 0) { if (beatmap.AudioEquals(playHistory[playHistoryIndex])) diff --git a/osu.Game/Screens/Select/CarouselContainer.cs b/osu.Game/Screens/Select/CarouselContainer.cs index e447693305..bd477f7fdd 100644 --- a/osu.Game/Screens/Select/CarouselContainer.cs +++ b/osu.Game/Screens/Select/CarouselContainer.cs @@ -95,6 +95,15 @@ namespace osu.Game.Screens.Select computeYPositions(); } + public void RemoveGroup(BeatmapGroup group) + { + groups.Remove(group); + scrollableContent.Remove(group.Header); + scrollableContent.Remove(group.BeatmapPanels); + + computeYPositions(); + } + private void movePanel(Panel panel, bool advance, bool animated, ref float currentY) { yPositions.Add(currentY); @@ -276,6 +285,12 @@ namespace osu.Game.Screens.Select if (direction == 0) return base.OnKeyDown(state, args); + SelectNext(direction, skipDifficulties); + return true; + } + + public void SelectNext(int direction = 1, bool skipDifficulties = true) + { if (!skipDifficulties) { int i = SelectedGroup.BeatmapPanels.IndexOf(SelectedPanel) + direction; @@ -284,7 +299,7 @@ namespace osu.Game.Screens.Select { //changing difficulty panel, not set. SelectGroup(SelectedGroup, SelectedGroup.BeatmapPanels[i]); - return true; + return; } } @@ -297,11 +312,9 @@ namespace osu.Game.Screens.Select if (groups[index].State != BeatmapGroupState.Hidden) { SelectBeatmap(groups[index].BeatmapPanels.First().Beatmap); - return true; + return; } } while (index != startIndex); - - return true; } public void SelectRandom() diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index bfa5697047..3911b26b00 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -138,7 +138,8 @@ namespace osu.Game.Screens.Select if (database == null) database = beatmaps; - database.BeatmapSetAdded += onDatabaseOnBeatmapSetAdded; + database.BeatmapSetAdded += onBeatmapSetAdded; + database.BeatmapSetRemoved += onBeatmapSetRemoved; trackManager = audio.Track; @@ -184,10 +185,9 @@ namespace osu.Game.Screens.Select }, 250); } - private void onDatabaseOnBeatmapSetAdded(BeatmapSetInfo s) - { - Schedule(() => addBeatmapSet(s, Game, true)); - } + private void onBeatmapSetAdded(BeatmapSetInfo s) => Schedule(() => addBeatmapSet(s, Game, true)); + + private void onBeatmapSetRemoved(BeatmapSetInfo s) => Schedule(() => removeBeatmapSet(s)); protected override void OnEntering(Screen last) { @@ -246,7 +246,8 @@ namespace osu.Game.Screens.Select if (playMode != null) playMode.ValueChanged -= playMode_ValueChanged; - database.BeatmapSetAdded -= onDatabaseOnBeatmapSetAdded; + database.BeatmapSetAdded -= onBeatmapSetAdded; + database.BeatmapSetRemoved -= onBeatmapSetRemoved; initialAddSetsTask.Cancel(); } @@ -278,7 +279,7 @@ namespace osu.Game.Screens.Select //todo: change background in selectionChanged instead; support per-difficulty backgrounds. changeBackground(beatmap); - carousel.SelectBeatmap(beatmap.BeatmapInfo); + carousel.SelectBeatmap(beatmap?.BeatmapInfo); } /// @@ -328,9 +329,7 @@ namespace osu.Game.Screens.Select b.ComputeDifficulty(database); beatmapSet.Beatmaps = beatmapSet.Beatmaps.OrderBy(b => b.StarDifficulty).ToList(); - var beatmap = new WorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault(), beatmapSet, database); - - var group = new BeatmapGroup(beatmap) + var group = new BeatmapGroup(database.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { SelectionChanged = selectionChanged, StartRequested = b => footer.StartButton.TriggerClick() @@ -355,9 +354,24 @@ namespace osu.Game.Screens.Select })); } + private void removeBeatmapSet(BeatmapSetInfo beatmapSet) + { + var group = beatmapGroups.Find(b => b.BeatmapSet.ID == beatmapSet.ID); + if (group == null) return; + + if (carousel.SelectedGroup == group) + carousel.SelectNext(); + + beatmapGroups.Remove(group); + carousel.RemoveGroup(group); + + if (beatmapGroups.Count == 0) + Beatmap = null; + } + private void addBeatmapSets(Framework.Game game, CancellationToken token) { - foreach (var beatmapSet in database.Query()) + foreach (var beatmapSet in database.Query().Where(b => !b.DeletePending)) { if (token.IsCancellationRequested) return; addBeatmapSet(beatmapSet, game); @@ -371,6 +385,13 @@ namespace osu.Game.Screens.Select case Key.Enter: footer.StartButton.TriggerClick(); return true; + case Key.Delete: + if (Beatmap != null) + { + Beatmap.Dispose(); + database.Delete(Beatmap.BeatmapSetInfo); + } + return true; } return base.OnKeyDown(state, args); diff --git a/osu.Game/Screens/Select/SearchTextBox.cs b/osu.Game/Screens/Select/SearchTextBox.cs index a640b575be..bb1edfd0d7 100644 --- a/osu.Game/Screens/Select/SearchTextBox.cs +++ b/osu.Game/Screens/Select/SearchTextBox.cs @@ -49,6 +49,15 @@ namespace osu.Game.Screens.Select } } + if (state.Keyboard.ShiftPressed) + { + switch (args.Key) + { + case Key.Delete: + return false; + } + } + return base.OnKeyDown(state, args); } }