mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 19:42:55 +08:00
Split out legacy import path from realm manager
This commit is contained in:
parent
c30e8047ab
commit
6b73f7c7ec
@ -11,6 +11,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Collections.IO
|
namespace osu.Game.Tests.Collections.IO
|
||||||
@ -187,7 +188,7 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
{
|
{
|
||||||
// intentionally spin this up on a separate task to avoid disposal deadlocks.
|
// intentionally spin this up on a separate task to avoid disposal deadlocks.
|
||||||
// see https://github.com/EventStore/EventStore/issues/1179
|
// see https://github.com/EventStore/EventStore/issues/1179
|
||||||
await Task.Factory.StartNew(() => osu.CollectionManager.Import(stream).WaitSafely(), TaskCreationOptions.LongRunning);
|
await Task.Factory.StartNew(() => new LegacyCollectionImporter(osu.CollectionManager).Import(stream).WaitSafely(), TaskCreationOptions.LongRunning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ namespace osu.Game.Tests
|
|||||||
if (withBeatmap)
|
if (withBeatmap)
|
||||||
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();
|
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();
|
||||||
|
|
||||||
AddInternal(CollectionManager = new CollectionManager(Storage));
|
AddInternal(CollectionManager = new CollectionManager());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
|
|
||||||
base.Content.AddRange(new Drawable[]
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
manager = new CollectionManager(LocalStorage),
|
manager = new CollectionManager(),
|
||||||
Content,
|
Content,
|
||||||
dialogOverlay = new DialogOverlay(),
|
dialogOverlay = new DialogOverlay(),
|
||||||
});
|
});
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
base.Content.AddRange(new Drawable[]
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
collectionManager = new CollectionManager(LocalStorage),
|
collectionManager = new CollectionManager(),
|
||||||
Content
|
Content
|
||||||
});
|
});
|
||||||
|
|
||||||
|
40
osu.Game/Beatmaps/RealmBeatmapCollection.cs
Normal file
40
osu.Game/Beatmaps/RealmBeatmapCollection.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps
|
||||||
|
{
|
||||||
|
public class RealmBeatmapCollection : RealmObject, IHasGuidPrimaryKey
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public Guid ID { get; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> BeatmapMD5Hashes { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date when this collection was last modified.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset LastModified { get; set; }
|
||||||
|
|
||||||
|
public RealmBeatmapCollection(string? name, List<string>? beatmapMD5Hashes)
|
||||||
|
{
|
||||||
|
ID = Guid.NewGuid();
|
||||||
|
Name = name ?? string.Empty;
|
||||||
|
BeatmapMD5Hashes = beatmapMD5Hashes ?? new List<string>();
|
||||||
|
|
||||||
|
LastModified = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
private RealmBeatmapCollection()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,11 +14,6 @@ namespace osu.Game.Collections
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BeatmapCollection
|
public class BeatmapCollection
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Invoked whenever any change occurs on this <see cref="BeatmapCollection"/>.
|
|
||||||
/// </summary>
|
|
||||||
public event Action Changed;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The collection's name.
|
/// The collection's name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -33,17 +28,5 @@ namespace osu.Game.Collections
|
|||||||
/// The date when this collection was last modified.
|
/// The date when this collection was last modified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
public BeatmapCollection()
|
|
||||||
{
|
|
||||||
BeatmapHashes.CollectionChanged += (_, _) => onChange();
|
|
||||||
Name.ValueChanged += _ => onChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onChange()
|
|
||||||
{
|
|
||||||
LastModifyDate = DateTimeOffset.Now;
|
|
||||||
Changed?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,346 +4,59 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Logging;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Framework.Platform;
|
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
|
||||||
using osu.Game.IO.Legacy;
|
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Collections
|
namespace osu.Game.Collections
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles user-defined collections of beatmaps.
|
/// Handles user-defined collections of beatmaps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// This is currently reading and writing from the osu-stable file format. This is a temporary arrangement until we refactor the
|
|
||||||
/// database backing the game. Going forward writing should be done in a similar way to other model stores.
|
|
||||||
/// </remarks>
|
|
||||||
public class CollectionManager : Component, IPostNotifications
|
public class CollectionManager : Component, IPostNotifications
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Database version in stable-compatible YYYYMMDD format.
|
|
||||||
/// </summary>
|
|
||||||
private const int database_version = 30000000;
|
|
||||||
|
|
||||||
private const string database_name = "collection.db";
|
|
||||||
private const string database_backup_name = "collection.db.bak";
|
|
||||||
|
|
||||||
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
|
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
|
||||||
|
|
||||||
private readonly Storage storage;
|
[Resolved]
|
||||||
|
private RealmAccess realm { get; set; }
|
||||||
public CollectionManager(Storage storage)
|
|
||||||
{
|
|
||||||
this.storage = storage;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
|
||||||
private DatabaseContextFactory efContextFactory { get; set; } = null!;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
efContextFactory?.WaitForMigrationCompletion();
|
|
||||||
|
|
||||||
Collections.CollectionChanged += collectionsChanged;
|
|
||||||
|
|
||||||
if (storage.Exists(database_backup_name))
|
|
||||||
{
|
|
||||||
// If a backup file exists, it means the previous write operation didn't run to completion.
|
|
||||||
// Always prefer the backup file in such a case as it's the most recent copy that is guaranteed to not be malformed.
|
|
||||||
//
|
|
||||||
// The database is saved 100ms after any change, and again when the game is closed, so there shouldn't be a large diff between the two files in the worst case.
|
|
||||||
if (storage.Exists(database_name))
|
|
||||||
storage.Delete(database_name);
|
|
||||||
File.Copy(storage.GetFullPath(database_backup_name), storage.GetFullPath(database_name));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storage.Exists(database_name))
|
|
||||||
{
|
|
||||||
List<BeatmapCollection> beatmapCollections;
|
|
||||||
|
|
||||||
using (var stream = storage.GetStream(database_name))
|
|
||||||
beatmapCollections = readCollections(stream);
|
|
||||||
|
|
||||||
// intentionally fire-and-forget async.
|
|
||||||
importCollections(beatmapCollections);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
base.LoadComplete();
|
||||||
|
|
||||||
|
realm.RegisterForNotifications(r => r.All<RealmBeatmapCollection>(), collectionsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectionsChanged(IRealmCollection<RealmBeatmapCollection> sender, ChangeSet changes, Exception error)
|
||||||
|
{
|
||||||
|
// TODO: hook up with realm changes.
|
||||||
|
|
||||||
|
if (changes == null)
|
||||||
{
|
{
|
||||||
case NotifyCollectionChangedAction.Add:
|
foreach (var collection in sender)
|
||||||
foreach (var c in e.NewItems.Cast<BeatmapCollection>())
|
Collections.Add(new BeatmapCollection
|
||||||
c.Changed += backgroundSave;
|
{
|
||||||
break;
|
Name = { Value = collection.Name },
|
||||||
|
BeatmapHashes = { Value = collection.BeatmapMD5Hashes },
|
||||||
case NotifyCollectionChangedAction.Remove:
|
});
|
||||||
foreach (var c in e.OldItems.Cast<BeatmapCollection>())
|
|
||||||
c.Changed -= backgroundSave;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Replace:
|
|
||||||
foreach (var c in e.OldItems.Cast<BeatmapCollection>())
|
|
||||||
c.Changed -= backgroundSave;
|
|
||||||
|
|
||||||
foreach (var c in e.NewItems.Cast<BeatmapCollection>())
|
|
||||||
c.Changed += backgroundSave;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
backgroundSave();
|
|
||||||
});
|
|
||||||
|
|
||||||
public Action<Notification> PostNotification { protected get; set; }
|
public Action<Notification> PostNotification { protected get; set; }
|
||||||
|
|
||||||
public Task<int> GetAvailableCount(StableStorage stableStorage)
|
|
||||||
{
|
|
||||||
if (!stableStorage.Exists(database_name))
|
|
||||||
return Task.FromResult(0);
|
|
||||||
|
|
||||||
return Task.Run(() =>
|
|
||||||
{
|
|
||||||
using (var stream = stableStorage.GetStream(database_name))
|
|
||||||
return readCollections(stream).Count;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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>
|
|
||||||
public Task ImportFromStableAsync(StableStorage stableStorage)
|
|
||||||
{
|
|
||||||
if (!stableStorage.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(async () =>
|
|
||||||
{
|
|
||||||
using (var stream = stableStorage.GetStream(database_name))
|
|
||||||
await Import(stream).ConfigureAwait(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Import(Stream stream)
|
|
||||||
{
|
|
||||||
var notification = new ProgressNotification
|
|
||||||
{
|
|
||||||
State = ProgressNotificationState.Active,
|
|
||||||
Text = "Collections import is initialising..."
|
|
||||||
};
|
|
||||||
|
|
||||||
PostNotification?.Invoke(notification);
|
|
||||||
|
|
||||||
var collections = readCollections(stream, notification);
|
|
||||||
await importCollections(collections).ConfigureAwait(false);
|
|
||||||
|
|
||||||
notification.CompletionText = $"Imported {collections.Count} collections";
|
|
||||||
notification.State = ProgressNotificationState.Completed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task importCollections(List<BeatmapCollection> newCollections)
|
|
||||||
{
|
|
||||||
var tcs = new TaskCompletionSource<bool>();
|
|
||||||
|
|
||||||
Schedule(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var newCol in newCollections)
|
|
||||||
{
|
|
||||||
var existing = Collections.FirstOrDefault(c => c.Name.Value == newCol.Name.Value);
|
|
||||||
if (existing == null)
|
|
||||||
Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
|
|
||||||
|
|
||||||
foreach (string newBeatmap in newCol.BeatmapHashes)
|
|
||||||
{
|
|
||||||
if (!existing.BeatmapHashes.Contains(newBeatmap))
|
|
||||||
existing.BeatmapHashes.Add(newBeatmap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tcs.SetResult(true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.Error(e, "Failed to import collection.");
|
|
||||||
tcs.SetException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<BeatmapCollection> readCollections(Stream stream, ProgressNotification notification = null)
|
|
||||||
{
|
|
||||||
if (notification != null)
|
|
||||||
{
|
|
||||||
notification.Text = "Reading collections...";
|
|
||||||
notification.Progress = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new List<BeatmapCollection>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var sr = new SerializationReader(stream))
|
|
||||||
{
|
|
||||||
sr.ReadInt32(); // Version
|
|
||||||
|
|
||||||
int collectionCount = sr.ReadInt32();
|
|
||||||
result.Capacity = collectionCount;
|
|
||||||
|
|
||||||
for (int i = 0; i < collectionCount; i++)
|
|
||||||
{
|
|
||||||
if (notification?.CancellationToken.IsCancellationRequested == true)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
var collection = new BeatmapCollection { Name = { Value = sr.ReadString() } };
|
|
||||||
int mapCount = sr.ReadInt32();
|
|
||||||
|
|
||||||
for (int j = 0; j < mapCount; j++)
|
|
||||||
{
|
|
||||||
if (notification?.CancellationToken.IsCancellationRequested == true)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
string checksum = sr.ReadString();
|
|
||||||
|
|
||||||
collection.BeatmapHashes.Add(checksum);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notification != null)
|
|
||||||
{
|
|
||||||
notification.Text = $"Imported {i + 1} of {collectionCount} collections";
|
|
||||||
notification.Progress = (float)(i + 1) / collectionCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.Error(e, "Failed to read collection database.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteAll()
|
public void DeleteAll()
|
||||||
{
|
{
|
||||||
Collections.Clear();
|
Collections.Clear();
|
||||||
PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!" });
|
PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!" });
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly object saveLock = new object();
|
|
||||||
private int lastSave;
|
|
||||||
private int saveFailures;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Perform a save with debounce.
|
|
||||||
/// </summary>
|
|
||||||
private void backgroundSave()
|
|
||||||
{
|
|
||||||
int current = Interlocked.Increment(ref lastSave);
|
|
||||||
Task.Delay(100).ContinueWith(_ =>
|
|
||||||
{
|
|
||||||
if (current != lastSave)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!save())
|
|
||||||
backgroundSave();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool save()
|
|
||||||
{
|
|
||||||
lock (saveLock)
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref lastSave);
|
|
||||||
|
|
||||||
// This is NOT thread-safe!!
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string tempPath = Path.GetTempFileName();
|
|
||||||
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
using (var sw = new SerializationWriter(ms, true))
|
|
||||||
{
|
|
||||||
sw.Write(database_version);
|
|
||||||
|
|
||||||
var collectionsCopy = Collections.ToArray();
|
|
||||||
sw.Write(collectionsCopy.Length);
|
|
||||||
|
|
||||||
foreach (var c in collectionsCopy)
|
|
||||||
{
|
|
||||||
sw.Write(c.Name.Value);
|
|
||||||
|
|
||||||
string[] beatmapsCopy = c.BeatmapHashes.ToArray();
|
|
||||||
|
|
||||||
sw.Write(beatmapsCopy.Length);
|
|
||||||
|
|
||||||
foreach (string b in beatmapsCopy)
|
|
||||||
sw.Write(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var fs = File.OpenWrite(tempPath))
|
|
||||||
ms.WriteTo(fs);
|
|
||||||
|
|
||||||
string databasePath = storage.GetFullPath(database_name);
|
|
||||||
string databaseBackupPath = storage.GetFullPath(database_backup_name);
|
|
||||||
|
|
||||||
// Back up the existing database, clearing any existing backup.
|
|
||||||
if (File.Exists(databaseBackupPath))
|
|
||||||
File.Delete(databaseBackupPath);
|
|
||||||
if (File.Exists(databasePath))
|
|
||||||
File.Move(databasePath, databaseBackupPath);
|
|
||||||
|
|
||||||
// Move the new database in-place of the existing one.
|
|
||||||
File.Move(tempPath, databasePath);
|
|
||||||
|
|
||||||
// If everything succeeded up to this point, remove the backup file.
|
|
||||||
if (File.Exists(databaseBackupPath))
|
|
||||||
File.Delete(databaseBackupPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saveFailures < 10)
|
|
||||||
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 (once) to indicate "actual" errors having occurred.
|
|
||||||
if (++saveFailures == 10)
|
|
||||||
Logger.Error(e, "Failed to save collection database!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
|
||||||
{
|
|
||||||
base.Dispose(isDisposing);
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
167
osu.Game/Database/LegacyCollectionImporter.cs
Normal file
167
osu.Game/Database/LegacyCollectionImporter.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Collections;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using osu.Game.IO.Legacy;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public class LegacyCollectionImporter
|
||||||
|
{
|
||||||
|
private readonly CollectionManager collections;
|
||||||
|
|
||||||
|
public LegacyCollectionImporter(CollectionManager collections)
|
||||||
|
{
|
||||||
|
this.collections = collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Action<Notification> PostNotification { protected get; set; }
|
||||||
|
|
||||||
|
private const string database_name = "collection.db";
|
||||||
|
|
||||||
|
public Task<int> GetAvailableCount(StableStorage stableStorage)
|
||||||
|
{
|
||||||
|
if (!stableStorage.Exists(database_name))
|
||||||
|
return Task.FromResult(0);
|
||||||
|
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
using (var stream = stableStorage.GetStream(database_name))
|
||||||
|
return readCollections(stream).Count;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||||
|
{
|
||||||
|
if (!stableStorage.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(async () =>
|
||||||
|
{
|
||||||
|
using (var stream = stableStorage.GetStream(database_name))
|
||||||
|
await Import(stream).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Import(Stream stream)
|
||||||
|
{
|
||||||
|
var notification = new ProgressNotification
|
||||||
|
{
|
||||||
|
State = ProgressNotificationState.Active,
|
||||||
|
Text = "Collections import is initialising..."
|
||||||
|
};
|
||||||
|
|
||||||
|
PostNotification?.Invoke(notification);
|
||||||
|
|
||||||
|
var importedCollections = readCollections(stream, notification);
|
||||||
|
await importCollections(importedCollections).ConfigureAwait(false);
|
||||||
|
|
||||||
|
notification.CompletionText = $"Imported {importedCollections.Count} collections";
|
||||||
|
notification.State = ProgressNotificationState.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task importCollections(List<BeatmapCollection> newCollections)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
// Schedule(() =>
|
||||||
|
// {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var newCol in newCollections)
|
||||||
|
{
|
||||||
|
var existing = collections.Collections.FirstOrDefault(c => c.Name.Value == newCol.Name.Value);
|
||||||
|
if (existing == null)
|
||||||
|
collections.Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
|
||||||
|
|
||||||
|
foreach (string newBeatmap in newCol.BeatmapHashes)
|
||||||
|
{
|
||||||
|
if (!existing.BeatmapHashes.Contains(newBeatmap))
|
||||||
|
existing.BeatmapHashes.Add(newBeatmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs.SetResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, "Failed to import collection.");
|
||||||
|
tcs.SetException(e);
|
||||||
|
}
|
||||||
|
// });
|
||||||
|
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BeatmapCollection> readCollections(Stream stream, ProgressNotification notification = null)
|
||||||
|
{
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
notification.Text = "Reading collections...";
|
||||||
|
notification.Progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<BeatmapCollection>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var sr = new SerializationReader(stream))
|
||||||
|
{
|
||||||
|
sr.ReadInt32(); // Version
|
||||||
|
|
||||||
|
int collectionCount = sr.ReadInt32();
|
||||||
|
result.Capacity = collectionCount;
|
||||||
|
|
||||||
|
for (int i = 0; i < collectionCount; i++)
|
||||||
|
{
|
||||||
|
if (notification?.CancellationToken.IsCancellationRequested == true)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var collection = new BeatmapCollection { Name = { Value = sr.ReadString() } };
|
||||||
|
int mapCount = sr.ReadInt32();
|
||||||
|
|
||||||
|
for (int j = 0; j < mapCount; j++)
|
||||||
|
{
|
||||||
|
if (notification?.CancellationToken.IsCancellationRequested == true)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
string checksum = sr.ReadString();
|
||||||
|
|
||||||
|
collection.BeatmapHashes.Add(checksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
notification.Text = $"Imported {i + 1} of {collectionCount} collections";
|
||||||
|
notification.Progress = (float)(i + 1) / collectionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, "Failed to read collection database.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -72,7 +72,7 @@ namespace osu.Game.Database
|
|||||||
return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage);
|
return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage);
|
||||||
|
|
||||||
case StableContent.Collections:
|
case StableContent.Collections:
|
||||||
return await collections.GetAvailableCount(stableStorage);
|
return await new LegacyCollectionImporter(collections).GetAvailableCount(stableStorage);
|
||||||
|
|
||||||
case StableContent.Scores:
|
case StableContent.Scores:
|
||||||
return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage);
|
return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage);
|
||||||
@ -109,7 +109,7 @@ namespace osu.Game.Database
|
|||||||
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
|
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
|
||||||
|
|
||||||
if (content.HasFlagFast(StableContent.Collections))
|
if (content.HasFlagFast(StableContent.Collections))
|
||||||
importTasks.Add(beatmapImportTask.ContinueWith(_ => collections.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(collections).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
||||||
|
|
||||||
if (content.HasFlagFast(StableContent.Scores))
|
if (content.HasFlagFast(StableContent.Scores))
|
||||||
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
||||||
|
@ -858,7 +858,7 @@ namespace osu.Game
|
|||||||
d.Origin = Anchor.TopRight;
|
d.Origin = Anchor.TopRight;
|
||||||
}), rightFloatingOverlayContent.Add, true);
|
}), rightFloatingOverlayContent.Add, true);
|
||||||
|
|
||||||
loadComponentSingleFile(new CollectionManager(Storage)
|
loadComponentSingleFile(new CollectionManager
|
||||||
{
|
{
|
||||||
PostNotification = n => Notifications.Post(n),
|
PostNotification = n => Notifications.Post(n),
|
||||||
}, Add, true);
|
}, Add, true);
|
||||||
|
Loading…
Reference in New Issue
Block a user