diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index c18cc30427..3f89866749 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; @@ -15,8 +18,16 @@ namespace osu.Game.Collections { public class CollectionManager : CompositeDrawable { + /// + /// Database version in YYYYMMDD format (matching stable). + /// + private const int database_version = 30000000; + private const string database_name = "collection.db"; + [Resolved] + private GameHost host { get; set; } + public IBindableList Collections => collections; private readonly BindableList collections = new BindableList(); @@ -24,13 +35,17 @@ namespace osu.Game.Collections private BeatmapManager beatmaps { get; set; } [BackgroundDependencyLoader] - private void load(GameHost host) + private void load() { if (host.Storage.Exists(database_name)) { using (var stream = host.Storage.GetStream(database_name)) collections.AddRange(readCollection(stream)); } + + foreach (var c in collections) + c.Changed += backgroundSave; + collections.CollectionChanged += (_, __) => backgroundSave(); } /// @@ -64,37 +79,112 @@ namespace osu.Game.Collections { var result = new List(); - using (var reader = new SerializationReader(stream)) + try { - reader.ReadInt32(); // Version - - int collectionCount = reader.ReadInt32(); - result.Capacity = collectionCount; - - for (int i = 0; i < collectionCount; i++) + using (var sr = new SerializationReader(stream)) { - var collection = new BeatmapCollection { Name = reader.ReadString() }; - int mapCount = reader.ReadInt32(); + sr.ReadInt32(); // Version - for (int j = 0; j < mapCount; j++) + int collectionCount = sr.ReadInt32(); + result.Capacity = collectionCount; + + for (int i = 0; i < collectionCount; i++) { - string checksum = reader.ReadString(); + var collection = new BeatmapCollection { Name = sr.ReadString() }; + int mapCount = sr.ReadInt32(); - var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum); - if (beatmap != null) - collection.Beatmaps.Add(beatmap); + for (int j = 0; j < mapCount; j++) + { + string checksum = sr.ReadString(); + + var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum); + if (beatmap != null) + collection.Beatmaps.Add(beatmap); + } + + result.Add(collection); } - - result.Add(collection); } } + catch (Exception e) + { + Logger.Error(e, "Failed to read collections"); + } return result; } + + private readonly object saveLock = new object(); + private int lastSave; + private int saveFailures; + + /// + /// Perform a save with debounce. + /// + private void backgroundSave() + { + var current = Interlocked.Increment(ref lastSave); + Task.Delay(100).ContinueWith(task => + { + if (current != lastSave) + return; + + if (!save()) + backgroundSave(); + }); + } + + private bool save() + { + lock (saveLock) + { + Interlocked.Increment(ref lastSave); + + try + { + // This is NOT thread-safe!! + + using (var sw = new SerializationWriter(host.Storage.GetStream(database_name, FileAccess.Write))) + { + sw.Write(database_version); + sw.Write(collections.Count); + + foreach (var c in collections) + { + sw.Write(c.Name); + sw.Write(c.Beatmaps.Count); + + foreach (var b in c.Beatmaps) + sw.Write(b.MD5Hash); + } + } + + saveFailures = 0; + return true; + } + catch (Exception e) + { + // Since this code is not thread-safe, we may run into random exceptions (such as collection enumeration or out of range indexing). + // Failures are thus only alerted if they exceed a threshold to indicate "actual" errors. + if (++saveFailures >= 10) + { + Logger.Error(e, "Failed to save collections"); + saveFailures = 0; + } + } + + return false; + } + } } public class BeatmapCollection { + /// + /// Invoked whenever any change occurs on this . + /// + public event Action Changed; + public string Name; public readonly BindableList Beatmaps = new BindableList(); @@ -105,7 +195,11 @@ namespace osu.Game.Collections { LastModifyTime = DateTimeOffset.UtcNow; - Beatmaps.CollectionChanged += (_, __) => LastModifyTime = DateTimeOffset.Now; + Beatmaps.CollectionChanged += (_, __) => + { + LastModifyTime = DateTimeOffset.Now; + Changed?.Invoke(); + }; } } }