2020-09-01 08:28:41 +00:00
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.IO;
|
2020-09-02 15:08:33 +00:00
|
|
|
using System.Linq;
|
2020-09-02 14:31:37 +00:00
|
|
|
using System.Threading;
|
|
|
|
using System.Threading.Tasks;
|
2020-09-02 15:08:33 +00:00
|
|
|
using osu.Framework;
|
2020-09-01 10:33:06 +00:00
|
|
|
using osu.Framework.Allocation;
|
|
|
|
using osu.Framework.Bindables;
|
|
|
|
using osu.Framework.Graphics.Containers;
|
2020-09-02 14:31:37 +00:00
|
|
|
using osu.Framework.Logging;
|
2020-09-01 08:28:41 +00:00
|
|
|
using osu.Framework.Platform;
|
|
|
|
using osu.Game.Beatmaps;
|
|
|
|
using osu.Game.IO.Legacy;
|
|
|
|
|
|
|
|
namespace osu.Game.Collections
|
|
|
|
{
|
2020-09-04 18:55:43 +00:00
|
|
|
public class BeatmapCollectionManager : CompositeDrawable
|
2020-09-01 08:28:41 +00:00
|
|
|
{
|
2020-09-02 14:31:37 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Database version in YYYYMMDD format (matching stable).
|
|
|
|
/// </summary>
|
|
|
|
private const int database_version = 30000000;
|
|
|
|
|
2020-09-01 10:33:06 +00:00
|
|
|
private const string database_name = "collection.db";
|
2020-09-01 08:28:41 +00:00
|
|
|
|
2020-09-02 15:08:33 +00:00
|
|
|
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
|
|
|
|
|
|
|
|
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
|
|
|
|
|
2020-09-02 14:31:37 +00:00
|
|
|
[Resolved]
|
|
|
|
private GameHost host { get; set; }
|
|
|
|
|
2020-09-01 10:33:06 +00:00
|
|
|
[Resolved]
|
|
|
|
private BeatmapManager beatmaps { get; set; }
|
|
|
|
|
|
|
|
[BackgroundDependencyLoader]
|
2020-09-02 14:31:37 +00:00
|
|
|
private void load()
|
2020-09-01 08:28:41 +00:00
|
|
|
{
|
2020-09-01 10:33:06 +00:00
|
|
|
if (host.Storage.Exists(database_name))
|
|
|
|
{
|
|
|
|
using (var stream = host.Storage.GetStream(database_name))
|
2020-09-02 15:08:33 +00:00
|
|
|
importCollections(readCollections(stream));
|
2020-09-01 10:33:06 +00:00
|
|
|
}
|
2020-09-02 14:31:37 +00:00
|
|
|
|
2020-09-02 15:08:33 +00:00
|
|
|
foreach (var c in Collections)
|
2020-09-02 14:31:37 +00:00
|
|
|
c.Changed += backgroundSave;
|
2020-09-02 15:08:33 +00:00
|
|
|
Collections.CollectionChanged += (_, __) => backgroundSave();
|
2020-09-01 08:28:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Set a storage with access to an osu-stable install for import purposes.
|
|
|
|
/// </summary>
|
|
|
|
public Func<Storage> GetStableStorage { private get; set; }
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
|
|
|
|
/// </summary>
|
2020-09-02 15:08:33 +00:00
|
|
|
public Task ImportFromStableAsync()
|
|
|
|
{
|
|
|
|
var stable = GetStableStorage?.Invoke();
|
|
|
|
|
|
|
|
if (stable == null)
|
|
|
|
{
|
|
|
|
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
|
|
|
|
return Task.CompletedTask;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!stable.Exists(database_name))
|
|
|
|
{
|
|
|
|
// This handles situations like when the user does not have a collections.db file
|
|
|
|
Logger.Log($"No {database_name} available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
|
|
|
|
return Task.CompletedTask;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Task.Run(() =>
|
|
|
|
{
|
|
|
|
var storage = GetStableStorage();
|
|
|
|
|
|
|
|
if (storage.Exists(database_name))
|
|
|
|
{
|
|
|
|
using (var stream = storage.GetStream(database_name))
|
|
|
|
{
|
|
|
|
var collection = readCollections(stream);
|
|
|
|
Schedule(() => importCollections(collection));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private void importCollections(List<BeatmapCollection> newCollections)
|
|
|
|
{
|
|
|
|
foreach (var newCol in newCollections)
|
|
|
|
{
|
|
|
|
var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name);
|
|
|
|
if (existing == null)
|
2020-09-04 19:43:51 +00:00
|
|
|
Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
|
2020-09-02 15:08:33 +00:00
|
|
|
|
|
|
|
foreach (var newBeatmap in newCol.Beatmaps)
|
|
|
|
{
|
|
|
|
if (!existing.Beatmaps.Contains(newBeatmap))
|
|
|
|
existing.Beatmaps.Add(newBeatmap);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private List<BeatmapCollection> readCollections(Stream stream)
|
2020-09-01 08:28:41 +00:00
|
|
|
{
|
|
|
|
var result = new List<BeatmapCollection>();
|
|
|
|
|
2020-09-02 14:31:37 +00:00
|
|
|
try
|
|
|
|
{
|
|
|
|
using (var sr = new SerializationReader(stream))
|
|
|
|
{
|
|
|
|
sr.ReadInt32(); // Version
|
|
|
|
|
|
|
|
int collectionCount = sr.ReadInt32();
|
|
|
|
result.Capacity = collectionCount;
|
|
|
|
|
|
|
|
for (int i = 0; i < collectionCount; i++)
|
|
|
|
{
|
2020-09-04 19:43:51 +00:00
|
|
|
var collection = new BeatmapCollection { Name = { Value = sr.ReadString() } };
|
2020-09-02 14:31:37 +00:00
|
|
|
int mapCount = sr.ReadInt32();
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
2020-09-01 08:28:41 +00:00
|
|
|
{
|
2020-09-02 14:47:42 +00:00
|
|
|
Logger.Error(e, "Failed to read collection database.");
|
2020-09-02 14:31:37 +00:00
|
|
|
}
|
2020-09-01 08:28:41 +00:00
|
|
|
|
2020-09-02 14:31:37 +00:00
|
|
|
return result;
|
|
|
|
}
|
2020-09-01 08:28:41 +00:00
|
|
|
|
2020-09-02 14:31:37 +00:00
|
|
|
private readonly object saveLock = new object();
|
|
|
|
private int lastSave;
|
|
|
|
private int saveFailures;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Perform a save with debounce.
|
|
|
|
/// </summary>
|
|
|
|
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
|
2020-09-01 08:28:41 +00:00
|
|
|
{
|
2020-09-02 14:31:37 +00:00
|
|
|
// This is NOT thread-safe!!
|
2020-09-01 08:28:41 +00:00
|
|
|
|
2020-09-02 14:31:37 +00:00
|
|
|
using (var sw = new SerializationWriter(host.Storage.GetStream(database_name, FileAccess.Write)))
|
2020-09-01 08:28:41 +00:00
|
|
|
{
|
2020-09-02 14:31:37 +00:00
|
|
|
sw.Write(database_version);
|
2020-09-02 15:08:33 +00:00
|
|
|
sw.Write(Collections.Count);
|
2020-09-01 08:28:41 +00:00
|
|
|
|
2020-09-02 15:08:33 +00:00
|
|
|
foreach (var c in Collections)
|
2020-09-02 14:31:37 +00:00
|
|
|
{
|
2020-09-04 19:43:51 +00:00
|
|
|
sw.Write(c.Name.Value);
|
2020-09-02 14:31:37 +00:00
|
|
|
sw.Write(c.Beatmaps.Count);
|
|
|
|
|
|
|
|
foreach (var b in c.Beatmaps)
|
|
|
|
sw.Write(b.MD5Hash);
|
|
|
|
}
|
2020-09-01 08:28:41 +00:00
|
|
|
}
|
2020-09-01 10:33:06 +00:00
|
|
|
|
2020-09-02 14:42:44 +00:00
|
|
|
if (saveFailures < 10)
|
|
|
|
saveFailures = 0;
|
2020-09-02 14:31:37 +00:00
|
|
|
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).
|
2020-09-02 14:42:44 +00:00
|
|
|
// Failures are thus only alerted if they exceed a threshold (once) to indicate "actual" errors having occurred.
|
|
|
|
if (++saveFailures == 10)
|
2020-09-02 14:47:42 +00:00
|
|
|
Logger.Error(e, "Failed to save collection database!");
|
2020-09-01 08:28:41 +00:00
|
|
|
}
|
|
|
|
|
2020-09-02 14:31:37 +00:00
|
|
|
return false;
|
|
|
|
}
|
2020-09-01 08:28:41 +00:00
|
|
|
}
|
2020-09-02 14:32:08 +00:00
|
|
|
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
|
|
{
|
|
|
|
base.Dispose(isDisposing);
|
|
|
|
save();
|
|
|
|
}
|
2020-09-01 08:28:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public class BeatmapCollection
|
|
|
|
{
|
2020-09-02 14:31:37 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Invoked whenever any change occurs on this <see cref="BeatmapCollection"/>.
|
|
|
|
/// </summary>
|
|
|
|
public event Action Changed;
|
|
|
|
|
2020-09-04 19:43:51 +00:00
|
|
|
public readonly Bindable<string> Name = new Bindable<string>();
|
2020-09-01 08:28:41 +00:00
|
|
|
|
2020-09-02 11:44:26 +00:00
|
|
|
public readonly BindableList<BeatmapInfo> Beatmaps = new BindableList<BeatmapInfo>();
|
2020-09-02 12:19:15 +00:00
|
|
|
|
|
|
|
public DateTimeOffset LastModifyTime { get; private set; }
|
|
|
|
|
|
|
|
public BeatmapCollection()
|
|
|
|
{
|
|
|
|
LastModifyTime = DateTimeOffset.UtcNow;
|
|
|
|
|
2020-09-02 14:31:37 +00:00
|
|
|
Beatmaps.CollectionChanged += (_, __) =>
|
|
|
|
{
|
|
|
|
LastModifyTime = DateTimeOffset.Now;
|
|
|
|
Changed?.Invoke();
|
|
|
|
};
|
2020-09-02 12:19:15 +00:00
|
|
|
}
|
2020-09-01 08:28:41 +00:00
|
|
|
}
|
|
|
|
}
|