Merge branch 'master' into bypass-local-metadata-cache

This commit is contained in:
Dean Herbert
2022-07-29 16:05:54 +09:00
committed by GitHub
85 changed files with 1366 additions and 1282 deletions

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Beatmaps
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio, RulesetStore rulesets)
{
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
}
[SetUpSteps]

View File

@ -5,12 +5,15 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Collections.IO
@ -29,7 +32,11 @@ namespace osu.Game.Tests.Collections.IO
await importCollectionsFromStream(osu, new MemoryStream());
Assert.That(osu.CollectionManager.Collections.Count, Is.Zero);
osu.Realm.Run(realm =>
{
var collections = realm.All<BeatmapCollection>().ToList();
Assert.That(collections.Count, Is.Zero);
});
}
finally
{
@ -49,18 +56,22 @@ namespace osu.Game.Tests.Collections.IO
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
osu.Realm.Run(realm =>
{
var collections = realm.All<BeatmapCollection>().ToList();
Assert.That(collections.Count, Is.EqualTo(2));
// Even with no beatmaps imported, collections are tracking the hashes and will continue to.
// In the future this whole mechanism will be replaced with having the collections in realm,
// but until that happens it makes rough sense that we want to track not-yet-imported beatmaps
// and have them associate with collections if/when they become available.
// Even with no beatmaps imported, collections are tracking the hashes and will continue to.
// In the future this whole mechanism will be replaced with having the collections in realm,
// but until that happens it makes rough sense that we want to track not-yet-imported beatmaps
// and have them associate with collections if/when they become available.
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(1));
Assert.That(collections[0].Name, Is.EqualTo("First"));
Assert.That(collections[0].BeatmapMD5Hashes.Count, Is.EqualTo(1));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(12));
Assert.That(collections[1].Name, Is.EqualTo("Second"));
Assert.That(collections[1].BeatmapMD5Hashes.Count, Is.EqualTo(12));
});
}
finally
{
@ -80,13 +91,18 @@ namespace osu.Game.Tests.Collections.IO
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
osu.Realm.Run(realm =>
{
var collections = realm.All<BeatmapCollection>().ToList();
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(1));
Assert.That(collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(12));
Assert.That(collections[0].Name, Is.EqualTo("First"));
Assert.That(collections[0].BeatmapMD5Hashes.Count, Is.EqualTo(1));
Assert.That(collections[1].Name, Is.EqualTo("Second"));
Assert.That(collections[1].BeatmapMD5Hashes.Count, Is.EqualTo(12));
});
}
finally
{
@ -123,7 +139,11 @@ namespace osu.Game.Tests.Collections.IO
}
Assert.That(exceptionThrown, Is.False);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0));
osu.Realm.Run(realm =>
{
var collections = realm.All<BeatmapCollection>().ToList();
Assert.That(collections.Count, Is.EqualTo(0));
});
}
finally
{
@ -148,12 +168,18 @@ namespace osu.Game.Tests.Collections.IO
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
// Move first beatmap from second collection into the first.
osu.CollectionManager.Collections[0].BeatmapHashes.Add(osu.CollectionManager.Collections[1].BeatmapHashes[0]);
osu.CollectionManager.Collections[1].BeatmapHashes.RemoveAt(0);
// ReSharper disable once MethodHasAsyncOverload
osu.Realm.Write(realm =>
{
var collections = realm.All<BeatmapCollection>().ToList();
// Rename the second collecction.
osu.CollectionManager.Collections[1].Name.Value = "Another";
// Move first beatmap from second collection into the first.
collections[0].BeatmapMD5Hashes.Add(collections[1].BeatmapMD5Hashes[0]);
collections[1].BeatmapMD5Hashes.RemoveAt(0);
// Rename the second collecction.
collections[1].Name = "Another";
});
}
finally
{
@ -168,13 +194,17 @@ namespace osu.Game.Tests.Collections.IO
{
var osu = LoadOsuIntoHost(host, true);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
osu.Realm.Run(realm =>
{
var collections = realm.All<BeatmapCollection>().ToList();
Assert.That(collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(2));
Assert.That(collections[0].Name, Is.EqualTo("First"));
Assert.That(collections[0].BeatmapMD5Hashes.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Another"));
Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(11));
Assert.That(collections[1].Name, Is.EqualTo("Another"));
Assert.That(collections[1].BeatmapMD5Hashes.Count, Is.EqualTo(11));
});
}
finally
{
@ -187,7 +217,7 @@ namespace osu.Game.Tests.Collections.IO
{
// intentionally spin this up on a separate task to avoid disposal deadlocks.
// 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.Realm).Import(stream).WaitSafely(), TaskCreationOptions.LongRunning);
}
}
}

View File

@ -9,6 +9,7 @@ using System.Linq.Expressions;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Models;
using osu.Game.Overlays.Notifications;
@ -432,6 +433,126 @@ namespace osu.Game.Tests.Database
});
}
/// <summary>
/// If all difficulties in the original beatmap set are in a collection, presume the user also wants new difficulties added.
/// </summary>
[TestCase(false)]
[TestCase(true)]
public void TestCollectionTransferNewBeatmap(bool allOriginalBeatmapsInCollection)
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// remove one difficulty before first import
directory.GetFiles("*.osu").First().Delete();
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathMissingOneBeatmap));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
int beatmapsToAddToCollection = 0;
importBeforeUpdate.PerformWrite(s =>
{
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1);
for (int i = 0; i < beatmapsToAddToCollection; i++)
beatmapCollection.BeatmapMD5Hashes.Add(s.Beatmaps[i].MD5Hash);
});
// Second import matches first but contains one extra .osu file.
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginal), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
importAfterUpdate.PerformRead(updated =>
{
updated.Realm.Refresh();
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
if (allOriginalBeatmapsInCollection)
{
Assert.That(updated.Beatmaps.Count, Is.EqualTo(beatmapsToAddToCollection + 1));
Assert.That(hashes, Has.Length.EqualTo(updated.Beatmaps.Count));
}
else
{
// Collection contains one less than the original beatmap, and two less after update (new difficulty included).
Assert.That(updated.Beatmaps.Count, Is.EqualTo(beatmapsToAddToCollection + 2));
Assert.That(hashes, Has.Length.EqualTo(beatmapsToAddToCollection));
}
});
});
}
/// <summary>
/// If a difficulty in the original beatmap set is modified, the updated version should remain in any collections it was in.
/// </summary>
[Test]
public void TestCollectionTransferModifiedBeatmap()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathModified, directory =>
{
// Modify one .osu file with different content.
var firstOsuFile = directory.GetFiles("*[Hard]*.osu").First();
string existingContent = File.ReadAllText(firstOsuFile.FullName);
File.WriteAllText(firstOsuFile.FullName, existingContent + "\n# I am new content");
});
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
string originalHash = string.Empty;
importBeforeUpdate.PerformWrite(s =>
{
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
beatmapCollection.BeatmapMD5Hashes.Add(originalHash);
});
// Second import matches first but contains a modified .osu file.
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathModified), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
importAfterUpdate.PerformRead(updated =>
{
updated.Realm.Refresh();
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
Assert.That(hashes, Has.Length.EqualTo(1));
Assert.That(hashes.First(), Is.EqualTo(updatedHash));
Assert.That(updatedHash, Is.Not.EqualTo(originalHash));
});
});
}
private static void checkCount<T>(RealmAccess realm, int expected, Expression<Func<T, bool>>? condition = null) where T : RealmObject
{
var query = realm.Realm.All<T>();

View File

@ -41,8 +41,6 @@ namespace osu.Game.Tests.Gameplay
AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
});
@ -58,8 +56,6 @@ namespace osu.Game.Tests.Gameplay
AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
});
@ -102,8 +98,6 @@ namespace osu.Game.Tests.Gameplay
AddStep("create container", () =>
{
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
working.LoadTrack();
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
gameplayClockContainer.Reset(startClock: !whileStopped);

View File

@ -69,7 +69,6 @@ namespace osu.Game.Tests.Gameplay
AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gameplayContainer = new MasterGameplayClockContainer(working, 0)
{
@ -96,7 +95,6 @@ namespace osu.Game.Tests.Gameplay
AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
const double start_time = 1000;

View File

@ -10,7 +10,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests
@ -47,7 +47,7 @@ namespace osu.Game.Tests
public class TestOsuGameBase : OsuGameBase
{
public CollectionManager CollectionManager { get; private set; }
public RealmAccess Realm => Dependencies.Get<RealmAccess>();
private readonly bool withBeatmap;
@ -62,8 +62,6 @@ namespace osu.Game.Tests
// Beatmap must be imported before the collection manager is loaded.
if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();
AddInternal(CollectionManager = new CollectionManager(Storage));
}
}
}

View File

@ -208,7 +208,7 @@ namespace osu.Game.Tests.Online
public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources,
GameHost host = null, WorkingBeatmap defaultBeatmap = null)
: base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap)
: base(storage, realm, api, audioManager, resources, host, defaultBeatmap)
{
}

View File

@ -32,7 +32,6 @@ namespace osu.Game.Tests.Skins
imported?.PerformRead(s =>
{
beatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]);
beatmap.LoadTrack();
});
}
@ -40,6 +39,10 @@ namespace osu.Game.Tests.Skins
public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null);
[Test]
public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => !(beatmap.Track is TrackVirtual));
public void TestRetrieveOggTrack() => AddAssert("track is non-null", () =>
{
using (var track = beatmap.LoadTrack())
return track is not TrackVirtual;
});
}
}

View File

@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Background
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage));
Dependencies.Cache(Realm);

View File

@ -1,8 +1,6 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -27,38 +25,32 @@ namespace osu.Game.Tests.Visual.Collections
{
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private DialogOverlay dialogOverlay;
private CollectionManager manager;
private RulesetStore rulesets;
private BeatmapManager beatmapManager;
private ManageCollectionsDialog dialog;
private DialogOverlay dialogOverlay = null!;
private BeatmapManager beatmapManager = null!;
private ManageCollectionsDialog dialog = null!;
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
base.Content.AddRange(new Drawable[]
{
manager = new CollectionManager(LocalStorage),
Content,
dialogOverlay = new DialogOverlay(),
});
Dependencies.Cache(manager);
Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
}
[SetUp]
public void SetUp() => Schedule(() =>
{
manager.Collections.Clear();
Realm.Write(r => r.RemoveAll<BeatmapCollection>());
Child = dialog = new ManageCollectionsDialog();
});
@ -78,17 +70,17 @@ namespace osu.Game.Tests.Visual.Collections
[Test]
public void TestLastItemIsPlaceholder()
{
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
AddAssert("last item is placeholder", () => !dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model.IsManaged);
}
[Test]
public void TestAddCollectionExternal()
{
AddStep("add collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "First collection" } }));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "First collection"))));
assertCollectionCount(1);
assertCollectionName(0, "First collection");
AddStep("add another collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "Second collection" } }));
AddStep("add another collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "Second collection"))));
assertCollectionCount(2);
assertCollectionName(1, "Second collection");
}
@ -108,7 +100,7 @@ namespace osu.Game.Tests.Visual.Collections
[Test]
public void TestAddCollectionViaPlaceholder()
{
DrawableCollectionListItem placeholderItem = null;
DrawableCollectionListItem placeholderItem = null!;
AddStep("focus placeholder", () =>
{
@ -116,24 +108,37 @@ namespace osu.Game.Tests.Visual.Collections
InputManager.Click(MouseButton.Left);
});
// Done directly via the collection since InputManager methods cannot add text to textbox...
AddStep("change collection name", () => placeholderItem.Model.Name.Value = "a");
assertCollectionCount(1);
AddAssert("collection now exists", () => manager.Collections.Contains(placeholderItem.Model));
assertCollectionCount(0);
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
AddStep("change collection name", () =>
{
placeholderItem.ChildrenOfType<TextBox>().First().Text = "test text";
InputManager.Key(Key.Enter);
});
assertCollectionCount(1);
AddAssert("last item is placeholder", () => !dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model.IsManaged);
}
[Test]
public void TestRemoveCollectionExternal()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
BeatmapCollection first = null!;
AddStep("remove first collection", () => manager.Collections.RemoveAt(0));
AddStep("add two collections", () =>
{
Realm.Write(r =>
{
r.Add(new[]
{
first = new BeatmapCollection(name: "1"),
new BeatmapCollection(name: "2"),
});
});
});
AddStep("remove first collection", () => Realm.Write(r => r.Remove(first)));
assertCollectionCount(1);
assertCollectionName(0, "2");
}
@ -143,7 +148,7 @@ namespace osu.Game.Tests.Visual.Collections
{
AddStep("add dropdown", () =>
{
Add(new CollectionFilterDropdown
Add(new CollectionDropdown
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@ -151,21 +156,27 @@ namespace osu.Game.Tests.Visual.Collections
Width = 0.4f,
});
});
AddStep("add two collections with same name", () => manager.Collections.AddRange(new[]
AddStep("add two collections with same name", () => Realm.Write(r => r.Add(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "1" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
}));
new BeatmapCollection(name: "1"),
new BeatmapCollection(name: "1")
{
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
},
})));
}
[Test]
public void TestRemoveCollectionViaButton()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
AddStep("add two collections", () => Realm.Write(r => r.Add(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
}));
new BeatmapCollection(name: "1"),
new BeatmapCollection(name: "2")
{
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
},
})));
assertCollectionCount(2);
@ -198,10 +209,13 @@ namespace osu.Game.Tests.Visual.Collections
[Test]
public void TestCollectionNotRemovedWhenDialogCancelled()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
AddStep("add collection", () => Realm.Write(r => r.Add(new[]
{
new BeatmapCollection { Name = { Value = "1" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
}));
new BeatmapCollection(name: "1")
{
BeatmapMD5Hashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash }
},
})));
assertCollectionCount(1);
@ -224,34 +238,67 @@ namespace osu.Game.Tests.Visual.Collections
[Test]
public void TestCollectionRenamedExternal()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
BeatmapCollection first = null!;
AddStep("add two collections", () =>
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
Realm.Write(r =>
{
r.Add(new[]
{
first = new BeatmapCollection(name: "1"),
new BeatmapCollection(name: "2"),
});
});
});
AddStep("change first collection name", () => manager.Collections[0].Name.Value = "First");
assertCollectionName(0, "1");
assertCollectionName(1, "2");
assertCollectionName(0, "First");
AddStep("change first collection name", () => Realm.Write(_ => first.Name = "First"));
// Item will have moved due to alphabetical sorting.
assertCollectionName(0, "2");
assertCollectionName(1, "First");
}
[Test]
public void TestCollectionRenamedOnTextChange()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
BeatmapCollection first = null!;
DrawableCollectionListItem firstItem = null!;
AddStep("add two collections", () =>
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
Realm.Write(r =>
{
r.Add(new[]
{
first = new BeatmapCollection(name: "1"),
new BeatmapCollection(name: "2"),
});
});
});
assertCollectionCount(2);
AddStep("change first collection name", () => dialog.ChildrenOfType<TextBox>().First().Text = "First");
AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First");
AddStep("focus first collection", () =>
{
InputManager.MoveMouseTo(firstItem = dialog.ChildrenOfType<DrawableCollectionListItem>().First());
InputManager.Click(MouseButton.Left);
});
AddStep("change first collection name", () =>
{
firstItem.ChildrenOfType<TextBox>().First().Text = "First";
InputManager.Key(Key.Enter);
});
AddUntilStep("collection has new name", () => first.Name == "First");
}
private void assertCollectionCount(int count)
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count(i => i.IsCreated.Value) == count);
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count() == count + 1); // +1 for placeholder
private void assertCollectionName(int index, string name)
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);

View File

@ -159,6 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().Alpha == 0);
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
AddStep("bind on update", () =>
{

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
}

View File

@ -14,6 +14,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
using osuTK.Input;
@ -33,6 +34,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
base.SetUpSteps();
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
AddStep("reload skin editor", () =>
{
skinEditor?.Expire();

View File

@ -33,7 +33,6 @@ namespace osu.Game.Tests.Visual.Gameplay
increment = skip_time;
var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
working.LoadTrack();
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0)
{

View File

@ -35,7 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected IScreen CurrentSubScreen => multiplayerComponents.MultiplayerScreen.CurrentSubScreen;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private TestMultiplayerComponents multiplayerComponents;
@ -45,8 +44,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}

View File

@ -19,29 +19,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private DrawableRoomParticipantsList list;
[SetUp]
public new void Setup() => Schedule(() =>
public override void SetUpSteps()
{
SelectedRoom.Value = new Room
{
Name = { Value = "test room" },
Host =
{
Value = new APIUser
{
Id = 2,
Username = "peppy",
}
}
};
base.SetUpSteps();
Child = list = new DrawableRoomParticipantsList
AddStep("create list", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
NumberOfCircles = 4
};
});
SelectedRoom.Value = new Room
{
Name = { Value = "test room" },
Host =
{
Value = new APIUser
{
Id = 2,
Username = "peppy",
}
}
};
Child = list = new DrawableRoomParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
NumberOfCircles = 4
};
});
}
[Test]
public void TestCircleCountNearLimit()

View File

@ -38,13 +38,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
private TestPlaylist playlist;
private BeatmapManager manager;
private RulesetStore rulesets;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}

View File

@ -25,23 +25,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RoomsContainer container;
[SetUp]
public new void Setup() => Schedule(() =>
public override void SetUpSteps()
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
base.SetUpSteps();
Child = container = new RoomsContainer
AddStep("create container", () =>
{
Child = new PopoverContainer
{
SelectedRoom = { BindTarget = SelectedRoom }
}
};
});
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Child = container = new RoomsContainer
{
SelectedRoom = { BindTarget = SelectedRoom }
}
};
});
}
[Test]
public void TestBasicListChanges()

View File

@ -3,7 +3,6 @@
#nullable disable
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@ -18,19 +17,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
public override void SetUpSteps()
{
SelectedRoom.Value = new Room();
base.SetUpSteps();
Child = new MatchBeatmapDetailArea
AddStep("create area", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500),
CreateNewItem = createNewItem
};
});
SelectedRoom.Value = new Room();
Child = new MatchBeatmapDetailArea
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500),
CreateNewItem = createNewItem
};
});
}
private void createNewItem()
{

View File

@ -4,8 +4,6 @@
#nullable disable
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@ -19,59 +17,62 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchLeaderboard : OnlinePlayTestScene
{
[BackgroundDependencyLoader]
private void load()
public override void SetUpSteps()
{
((DummyAPIAccess)API).HandleRequest = r =>
base.SetUpSteps();
AddStep("setup API", () =>
{
switch (r)
((DummyAPIAccess)API).HandleRequest = r =>
{
case GetRoomLeaderboardRequest leaderboardRequest:
leaderboardRequest.TriggerSuccess(new APILeaderboard
{
Leaderboard = new List<APIUserScoreAggregate>
switch (r)
{
case GetRoomLeaderboardRequest leaderboardRequest:
leaderboardRequest.TriggerSuccess(new APILeaderboard
{
new APIUserScoreAggregate
Leaderboard = new List<APIUserScoreAggregate>
{
UserID = 2,
User = new APIUser { Id = 2, Username = "peppy" },
TotalScore = 995533,
RoomID = 3,
CompletedBeatmaps = 1,
TotalAttempts = 6,
Accuracy = 0.9851
},
new APIUserScoreAggregate
{
UserID = 1040328,
User = new APIUser { Id = 1040328, Username = "smoogipoo" },
TotalScore = 981100,
RoomID = 3,
CompletedBeatmaps = 1,
TotalAttempts = 9,
Accuracy = 0.937
new APIUserScoreAggregate
{
UserID = 2,
User = new APIUser { Id = 2, Username = "peppy" },
TotalScore = 995533,
RoomID = 3,
CompletedBeatmaps = 1,
TotalAttempts = 6,
Accuracy = 0.9851
},
new APIUserScoreAggregate
{
UserID = 1040328,
User = new APIUser { Id = 1040328, Username = "smoogipoo" },
TotalScore = 981100,
RoomID = 3,
CompletedBeatmaps = 1,
TotalAttempts = 9,
Accuracy = 0.937
}
}
}
});
return true;
}
});
return true;
}
return false;
};
}
return false;
};
});
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room { RoomID = { Value = 3 } };
Child = new MatchLeaderboard
AddStep("create leaderboard", () =>
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = MatchLeaderboardScope.Overall,
};
});
SelectedRoom.Value = new Room { RoomID = { Value = 3 } };
Child = new MatchLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = MatchLeaderboardScope.Overall,
};
});
}
}
}

View File

@ -22,8 +22,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiSpectatorLeaderboard leaderboard;
[SetUpSteps]
public new void SetUpSteps()
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("reset", () =>
{
leaderboard?.RemoveAndDisposeImmediately();

View File

@ -56,8 +56,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedBeatmapId = importedBeatmap.OnlineID;
}
[SetUp]
public new void Setup() => Schedule(() => playingUsers.Clear());
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("clear playing users", () => playingUsers.Clear());
}
[Test]
public void TestDelayedStart()

View File

@ -49,7 +49,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayer : ScreenTestScene
{
private BeatmapManager beatmaps = null!;
private RulesetStore rulesets = null!;
private BeatmapSetInfo importedSet = null!;
private TestMultiplayerComponents multiplayerComponents = null!;
@ -63,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}

View File

@ -3,7 +3,6 @@
#nullable disable
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -13,23 +12,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
public override void SetUpSteps()
{
Child = new PopoverContainer
base.SetUpSteps();
AddStep("create footer", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = new Container
Child = new PopoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 50,
Child = new MultiplayerMatchFooter()
}
};
});
RelativeSizeAxes = Axes.Both,
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 50,
Child = new MultiplayerMatchFooter()
}
};
});
}
}
}

View File

@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()));

View File

@ -40,7 +40,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiplayerMatchSubScreen screen;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
public TestSceneMultiplayerMatchSubScreen()
@ -51,8 +50,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
@ -60,16 +59,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room { Name = { Value = "Test Room" } };
});
[SetUpSteps]
public void SetupSteps()
{
AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(SelectedRoom.Value)));
AddStep("load match", () =>
{
SelectedRoom.Value = new Room { Name = { Value = "Test Room" } };
LoadScreen(screen = new MultiplayerMatchSubScreen(SelectedRoom.Value));
});
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
}

View File

@ -31,33 +31,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerPlaylist list;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = list = new MultiplayerPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f, 0.8f)
};
});
[SetUpSteps]
public new void SetUpSteps()
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("create list", () =>
{
Child = list = new MultiplayerPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f, 0.8f)
};
});
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();

View File

@ -29,15 +29,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerQueueList playlist;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}

View File

@ -35,55 +35,58 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapSetInfo importedSet;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
[SetUp]
public new void Setup() => Schedule(() =>
public override void SetUpSteps()
{
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
base.SetUpSteps();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
AddStep("create button", () =>
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
};
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
};
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
spectateButton = new MultiplayerSpectateButton
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
},
startControl = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
spectateButton = new MultiplayerSpectateButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
},
startControl = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
}
}
}
}
};
});
};
});
}
[TestCase(MultiplayerRoomState.Open)]
[TestCase(MultiplayerRoomState.WaitingForLoad)]

View File

@ -28,15 +28,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private BeatmapManager manager;
private RulesetStore rulesets;
private TestPlaylistsSongSelect songSelect;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
var beatmapSet = TestResources.CreateTestBeatmapSetInfo();

View File

@ -14,17 +14,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneStarRatingRangeDisplay : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
public override void SetUpSteps()
{
SelectedRoom.Value = new Room();
base.SetUpSteps();
Child = new StarRatingRangeDisplay
AddStep("create display", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
});
SelectedRoom.Value = new Room();
Child = new StarRatingRangeDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
});
}
[Test]
public void TestRange([Values(0, 2, 3, 4, 6, 7)] double min, [Values(0, 2, 3, 4, 6, 7)] double max)

View File

@ -30,7 +30,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneTeamVersus : ScreenTestScene
{
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private TestMultiplayerComponents multiplayerComponents;
@ -40,8 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}

View File

@ -74,14 +74,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("set filter again", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
AddStep("open collections dropdown", () =>
{
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionFilterDropdown>().Single());
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionDropdown>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("press back once", () => InputManager.Click(MouseButton.Button1));
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
AddAssert("collections dropdown closed", () => songSelect
.ChildrenOfType<CollectionFilterDropdown>().Single()
.ChildrenOfType<CollectionDropdown>().Single()
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));

View File

@ -25,17 +25,21 @@ namespace osu.Game.Tests.Visual.Playlists
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
[SetUp]
public new void Setup() => Schedule(() =>
public override void SetUpSteps()
{
SelectedRoom.Value = new Room();
base.SetUpSteps();
Child = settings = new TestRoomSettings(SelectedRoom.Value)
AddStep("create overlay", () =>
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible }
};
});
SelectedRoom.Value = new Room();
Child = settings = new TestRoomSettings(SelectedRoom.Value)
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible }
};
});
}
[Test]
public void TestButtonEnabledOnlyWithNameAndBeatmap()

View File

@ -15,21 +15,25 @@ namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsParticipantsList : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
public override void SetUpSteps()
{
SelectedRoom.Value = new Room { RoomID = { Value = 7 } };
base.SetUpSteps();
for (int i = 0; i < 50; i++)
AddStep("create list", () =>
{
SelectedRoom.Value.RecentParticipants.Add(new APIUser
SelectedRoom.Value = new Room { RoomID = { Value = 7 } };
for (int i = 0; i < 50; i++)
{
Username = "peppy",
Statistics = new UserStatistics { GlobalRank = 1234 },
Id = 2
});
}
});
SelectedRoom.Value.RecentParticipants.Add(new APIUser
{
Username = "peppy",
Statistics = new UserStatistics { GlobalRank = 1234 },
Id = 2
});
}
});
}
[Test]
public void TestHorizontalLayout()

View File

@ -32,7 +32,6 @@ namespace osu.Game.Tests.Visual.Playlists
public class TestScenePlaylistsRoomCreation : OnlinePlayTestScene
{
private BeatmapManager manager;
private RulesetStore rulesets;
private TestPlaylistsRoomSubScreen match;
@ -41,8 +40,8 @@ namespace osu.Game.Tests.Visual.Playlists
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);

View File

@ -1,8 +1,6 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -28,35 +26,28 @@ namespace osu.Game.Tests.Visual.SongSelect
{
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private CollectionManager collectionManager;
private RulesetStore rulesets;
private BeatmapManager beatmapManager;
private FilterControl control;
private BeatmapManager beatmapManager = null!;
private FilterControl control = null!;
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
base.Content.AddRange(new Drawable[]
{
collectionManager = new CollectionManager(LocalStorage),
Content
});
Dependencies.Cache(collectionManager);
}
[SetUp]
public void SetUp() => Schedule(() =>
{
collectionManager.Collections.Clear();
Realm.Write(r => r.RemoveAll<BeatmapCollection>());
Child = control = new FilterControl
{
@ -77,8 +68,8 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestCollectionAddedToDropdown()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "2"))));
assertCollectionDropdownContains("1");
assertCollectionDropdownContains("2");
}
@ -86,9 +77,11 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestCollectionRemovedFromDropdown()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
AddStep("remove collection", () => collectionManager.Collections.RemoveAt(0));
BeatmapCollection first = null!;
AddStep("add collection", () => Realm.Write(r => r.Add(first = new BeatmapCollection(name: "1"))));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("remove collection", () => Realm.Write(r => r.Remove(first)));
assertCollectionDropdownContains("1", false);
assertCollectionDropdownContains("2");
@ -97,16 +90,16 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestCollectionRenamed()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("select collection", () =>
{
var dropdown = control.ChildrenOfType<CollectionFilterDropdown>().Single();
var dropdown = control.ChildrenOfType<CollectionDropdown>().Single();
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
});
addExpandHeaderStep();
AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First");
AddStep("change name", () => Realm.Write(_ => getFirstCollection().Name = "First"));
assertCollectionDropdownContains("First");
assertCollectionHeaderDisplays("First");
@ -124,7 +117,7 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestCollectionFilterHasAddButton()
{
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
}
@ -134,7 +127,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
@ -150,13 +143,13 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
AddStep("add beatmap to collection", () => collectionManager.Collections[0].BeatmapHashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash));
AddStep("add beatmap to collection", () => Realm.Write(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash)));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
AddStep("remove beatmap from collection", () => collectionManager.Collections[0].BeatmapHashes.Clear());
AddStep("remove beatmap from collection", () => Realm.Write(r => getFirstCollection().BeatmapMD5Hashes.Clear()));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
}
@ -167,24 +160,26 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1"))));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
addClickAddOrRemoveButtonStep(1);
AddAssert("collection contains beatmap", () => collectionManager.Collections[0].BeatmapHashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
addClickAddOrRemoveButtonStep(1);
AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].BeatmapHashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
}
[Test]
public void TestManageCollectionsFilterIsNotSelected()
{
bool received = false;
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => Realm.Write(r => r.Add(new BeatmapCollection(name: "1", new List<string> { "abc" }))));
AddStep("select collection", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1));
@ -193,18 +188,28 @@ namespace osu.Game.Tests.Visual.SongSelect
addExpandHeaderStep();
AddStep("watch for filter requests", () =>
{
received = false;
control.ChildrenOfType<CollectionDropdown>().First().RequestFilter = () => received = true;
});
AddStep("click manage collections filter", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItems().Last());
InputManager.Click(MouseButton.Left);
});
AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name.Value == "1");
AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes.Any());
AddAssert("filter request not fired", () => !received);
}
private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All<BeatmapCollection>().First());
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
=> AddAssert($"collection dropdown header displays '{collectionName}'",
() => shouldDisplay == (control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
() => shouldDisplay == (control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
@ -216,7 +221,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void addExpandHeaderStep() => AddStep("expand header", () =>
{
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single());
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionDropdown.CollectionDropdownHeader>().Single());
InputManager.Click(MouseButton.Left);
});
@ -227,6 +232,6 @@ namespace osu.Game.Tests.Visual.SongSelect
});
private IEnumerable<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
=> control.ChildrenOfType<CollectionFilterDropdown>().Single().ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
=> control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
}
}

View File

@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.SongSelect
// At a point we have isolated interactive test runs enough, this can likely be removed.
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(Realm);
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default));
Dependencies.Cache(music = new MusicController());

View File

@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);

View File

@ -37,7 +37,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly ContextMenuContainer contextMenuContainer;
private readonly BeatmapLeaderboard leaderboard;
private RulesetStore rulesetStore;
private BeatmapManager beatmapManager;
private ScoreManager scoreManager;
@ -72,8 +71,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);

View File

@ -14,9 +14,6 @@ using osu.Game.IO.Serialization.Converters;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A Beatmap containing converted HitObjects.
/// </summary>
public class Beatmap<T> : IBeatmap<T>
where T : HitObject
{

View File

@ -14,6 +14,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Formats;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
@ -71,6 +72,8 @@ namespace osu.Game.Beatmaps
// Transfer local values which should be persisted across a beatmap update.
updated.DateAdded = original.DateAdded;
transferCollectionReferences(realm, original, updated);
foreach (var beatmap in original.Beatmaps.ToArray())
{
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.Hash);
@ -112,6 +115,40 @@ namespace osu.Game.Beatmaps
return first;
}
private static void transferCollectionReferences(Realm realm, BeatmapSetInfo original, BeatmapSetInfo updated)
{
// First check if every beatmap in the original set is in any collections.
// In this case, we will assume they also want any newly added difficulties added to the collection.
foreach (var c in realm.All<BeatmapCollection>())
{
if (original.Beatmaps.Select(b => b.MD5Hash).All(c.BeatmapMD5Hashes.Contains))
{
foreach (var b in original.Beatmaps)
c.BeatmapMD5Hashes.Remove(b.MD5Hash);
foreach (var b in updated.Beatmaps)
c.BeatmapMD5Hashes.Add(b.MD5Hash);
}
}
// Handle collections using permissive difficulty name to track difficulties.
foreach (var originalBeatmap in original.Beatmaps)
{
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName);
if (updatedBeatmap == null)
continue;
var collections = realm.All<BeatmapCollection>().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(originalBeatmap.MD5Hash));
foreach (var c in collections)
{
c.BeatmapMD5Hashes.Remove(originalBeatmap.MD5Hash);
c.BeatmapMD5Hashes.Add(updatedBeatmap.MD5Hash);
}
}
}
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)

View File

@ -20,8 +20,12 @@ using Realms;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A single beatmap difficulty.
/// A realm model containing metadata for a single beatmap difficulty.
/// This should generally include anything which is required to be filtered on at song select, or anything pertaining to storage of beatmaps in the client.
/// </summary>
/// <remarks>
/// There are some legacy fields in this model which are not persisted to realm. These are isolated in a code region within the class and should eventually be migrated to `Beatmap`.
/// </remarks>
[ExcludeFromDynamicCompile]
[Serializable]
[MapTo("Beatmap")]

View File

@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps
public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null,
public BeatmapManager(Storage storage, RealmAccess realm, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null,
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
: base(storage, realm)
{

View File

@ -12,6 +12,17 @@ using Realms;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A realm model containing metadata for a beatmap.
/// </summary>
/// <remarks>
/// This is currently stored against each beatmap difficulty, even when it is duplicated.
/// It is also provided via <see cref="BeatmapSetInfo"/> for convenience and historical purposes.
/// A future effort could see this converted to an <see cref="EmbeddedObject"/> or potentially de-duped
/// and shared across multiple difficulties in the same set, if required.
///
/// Note that difficulty name is not stored in this metadata but in <see cref="BeatmapInfo"/>.
/// </remarks>
[ExcludeFromDynamicCompile]
[Serializable]
[MapTo("BeatmapMetadata")]

View File

@ -14,6 +14,9 @@ using Realms;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A realm model containing metadata for a beatmap set (containing multiple <see cref="BeatmapInfo"/>s).
/// </summary>
[ExcludeFromDynamicCompile]
[MapTo("BeatmapSet")]
public class BeatmapSetInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo

View File

@ -44,6 +44,10 @@ namespace osu.Game.Beatmaps
}, audio)
{
this.textures = textures;
// We are guaranteed to have a virtual track.
// To ease usability, ensure the track is available from point of construction.
LoadTrack();
}
protected override IBeatmap GetBeatmap() => new Beatmap();

View File

@ -11,6 +11,10 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A materialised beatmap.
/// Generally this interface will be implemented alongside <see cref="IBeatmap{T}"/>, which exposes the ruleset-typed hit objects.
/// </summary>
public interface IBeatmap
{
/// <summary>
@ -65,6 +69,9 @@ namespace osu.Game.Beatmaps
IBeatmap Clone();
}
/// <summary>
/// A materialised beatmap containing converted HitObjects.
/// </summary>
public interface IBeatmap<out T> : IBeatmap
where T : HitObject
{

View File

@ -18,7 +18,12 @@ using osu.Game.Storyboards;
namespace osu.Game.Beatmaps
{
/// <summary>
/// Provides access to the multiple resources offered by a beatmap model (textures, skins, playable beatmaps etc.)
/// A more expensive representation of a beatmap which allows access to various associated resources.
/// - Access textures and other resources via <see cref="GetStream"/>.
/// - Access the storyboard via <see cref="Storyboard"/>.
/// - Access a local skin via <see cref="Skin"/>.
/// - Access the track via <see cref="LoadTrack"/> (and then <see cref="Track"/> for subsequent accesses).
/// - Create a playable <see cref="Beatmap"/> via <see cref="GetPlayableBeatmap(osu.Game.Rulesets.IRulesetInfo,System.Collections.Generic.IReadOnlyList{osu.Game.Rulesets.Mods.Mod})"/>.
/// </summary>
public interface IWorkingBeatmap
{

View File

@ -1,49 +1,57 @@
// 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 osu.Framework.Bindables;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Database;
using Realms;
namespace osu.Game.Collections
{
/// <summary>
/// A collection of beatmaps grouped by a name.
/// </summary>
public class BeatmapCollection
public class BeatmapCollection : RealmObject, IHasGuidPrimaryKey
{
/// <summary>
/// Invoked whenever any change occurs on this <see cref="BeatmapCollection"/>.
/// </summary>
public event Action Changed;
[PrimaryKey]
public Guid ID { get; set; }
/// <summary>
/// The collection's name.
/// </summary>
public readonly Bindable<string> Name = new Bindable<string>();
public string Name { get; set; } = string.Empty;
/// <summary>
/// The <see cref="BeatmapInfo.MD5Hash"/>es of beatmaps contained by the collection.
/// </summary>
public readonly BindableList<string> BeatmapHashes = new BindableList<string>();
/// <remarks>
/// We store as hashes rather than references to <see cref="BeatmapInfo"/>s to allow collections to maintain
/// references to beatmaps even if they are removed. This helps with cases like importing collections before
/// importing the beatmaps they contain, or when sharing collections between users.
///
/// This can probably change in the future as we build the system up.
/// </remarks>
public IList<string> BeatmapMD5Hashes { get; } = null!;
/// <summary>
/// The date when this collection was last modified.
/// </summary>
public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow;
public DateTimeOffset LastModified { get; set; }
public BeatmapCollection()
public BeatmapCollection(string? name = null, IList<string>? beatmapMD5Hashes = null)
{
BeatmapHashes.CollectionChanged += (_, _) => onChange();
Name.ValueChanged += _ => onChange();
ID = Guid.NewGuid();
Name = name ?? string.Empty;
BeatmapMD5Hashes = beatmapMD5Hashes ?? new List<string>();
LastModified = DateTimeOffset.UtcNow;
}
private void onChange()
[UsedImplicitly]
private BeatmapCollection()
{
LastModifyDate = DateTimeOffset.Now;
Changed?.Invoke();
}
}
}

View File

@ -0,0 +1,250 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK;
using Realms;
namespace osu.Game.Collections
{
/// <summary>
/// A dropdown to select the collection to be used to filter results.
/// </summary>
public class CollectionDropdown : OsuDropdown<CollectionFilterMenuItem>
{
/// <summary>
/// Whether to show the "manage collections..." menu item in the dropdown.
/// </summary>
protected virtual bool ShowManageCollectionsItem => true;
public Action? RequestFilter { private get; set; }
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
[Resolved]
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
[Resolved]
private RealmAccess realm { get; set; } = null!;
private IDisposable? realmSubscription;
public CollectionDropdown()
{
ItemSource = filters;
Current.Value = new AllBeatmapsCollectionFilterMenuItem();
}
protected override void LoadComplete()
{
base.LoadComplete();
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
Current.BindValueChanged(selectionChanged);
}
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes, Exception error)
{
var selectedItem = SelectedItem?.Value?.Collection;
var allBeatmaps = new AllBeatmapsCollectionFilterMenuItem();
filters.Clear();
filters.Add(allBeatmaps);
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
if (ShowManageCollectionsItem)
filters.Add(new ManageCollectionsFilterMenuItem());
// This current update and schedule is required to work around dropdown headers not updating text even when the selected item
// changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue
// a warning that it's going to be a frustrating journey.
Current.Value = allBeatmaps;
Schedule(() => Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0]);
// Trigger a re-filter if the current item was in the change set.
if (selectedItem != null && changes != null)
{
foreach (int index in changes.ModifiedIndices)
{
if (collections[index].ID == selectedItem.ID)
RequestFilter?.Invoke();
}
}
}
private Live<BeatmapCollection>? lastFiltered;
private void selectionChanged(ValueChangedEvent<CollectionFilterMenuItem> filter)
{
// May be null during .Clear().
if (filter.NewValue == null)
return;
// Never select the manage collection filter - rollback to the previous filter.
// This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value.
if (filter.NewValue is ManageCollectionsFilterMenuItem)
{
Current.Value = filter.OldValue;
manageCollectionsDialog?.Show();
return;
}
var newCollection = filter.NewValue?.Collection;
// This dropdown be weird.
// We only care about filtering if the actual collection has changed.
if (newCollection != lastFiltered)
{
RequestFilter?.Invoke();
lastFiltered = newCollection;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
realmSubscription?.Dispose();
}
protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName;
protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader();
protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu();
protected virtual CollectionDropdownHeader CreateCollectionHeader() => new CollectionDropdownHeader();
protected virtual CollectionDropdownMenu CreateCollectionMenu() => new CollectionDropdownMenu();
public class CollectionDropdownHeader : OsuDropdownHeader
{
public CollectionDropdownHeader()
{
Height = 25;
Icon.Size = new Vector2(16);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 };
}
}
protected class CollectionDropdownMenu : OsuDropdownMenu
{
public CollectionDropdownMenu()
{
MaxHeight = 200;
}
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownDrawableMenuItem(item)
{
BackgroundColourHover = HoverColour,
BackgroundColourSelected = SelectionColour
};
}
protected class CollectionDropdownDrawableMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
{
private IconButton addOrRemoveButton = null!;
private bool beatmapInCollection;
private readonly Live<BeatmapCollection>? collection;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
public CollectionDropdownDrawableMenuItem(MenuItem item)
: base(item)
{
collection = ((DropdownMenuItem<CollectionFilterMenuItem>)item).Value.Collection;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(addOrRemoveButton = new IconButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
X = -OsuScrollContainer.SCROLL_BAR_HEIGHT,
Scale = new Vector2(0.65f),
Action = addOrRemove,
});
}
protected override void LoadComplete()
{
base.LoadComplete();
if (collection != null)
{
beatmap.BindValueChanged(_ =>
{
beatmapInCollection = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash));
addOrRemoveButton.Enabled.Value = !beatmap.IsDefault;
addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare;
addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap";
updateButtonVisibility();
}, true);
}
updateButtonVisibility();
}
protected override bool OnHover(HoverEvent e)
{
updateButtonVisibility();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateButtonVisibility();
base.OnHoverLost(e);
}
protected override void OnSelectChange()
{
base.OnSelectChange();
updateButtonVisibility();
}
private void updateButtonVisibility()
{
if (collection == null)
addOrRemoveButton.Alpha = 0;
else
addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0;
}
private void addOrRemove()
{
Debug.Assert(collection != null);
collection.PerformWrite(c =>
{
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
});
}
protected override Drawable CreateContent() => (Content)base.CreateContent();
}
}
}

View File

@ -1,297 +0,0 @@
// 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.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Collections
{
/// <summary>
/// A dropdown to select the <see cref="CollectionFilterMenuItem"/> to filter beatmaps using.
/// </summary>
public class CollectionFilterDropdown : OsuDropdown<CollectionFilterMenuItem>
{
/// <summary>
/// Whether to show the "manage collections..." menu item in the dropdown.
/// </summary>
protected virtual bool ShowManageCollectionsItem => true;
private readonly BindableWithCurrent<CollectionFilterMenuItem> current = new BindableWithCurrent<CollectionFilterMenuItem>();
public new Bindable<CollectionFilterMenuItem> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly IBindableList<BeatmapCollection> collections = new BindableList<BeatmapCollection>();
private readonly IBindableList<string> beatmaps = new BindableList<string>();
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
public CollectionFilterDropdown()
{
ItemSource = filters;
Current.Value = new AllBeatmapsCollectionFilterMenuItem();
}
protected override void LoadComplete()
{
base.LoadComplete();
if (collectionManager != null)
collections.BindTo(collectionManager.Collections);
// Dropdown has logic which triggers a change on the bindable with every change to the contained items.
// This is not desirable here, as it leads to multiple filter operations running even though nothing has changed.
// An extra bindable is enough to subvert this behaviour.
base.Current = Current;
collections.BindCollectionChanged((_, _) => collectionsChanged(), true);
Current.BindValueChanged(filterChanged, true);
}
/// <summary>
/// Occurs when a collection has been added or removed.
/// </summary>
private void collectionsChanged()
{
var selectedItem = SelectedItem?.Value?.Collection;
filters.Clear();
filters.Add(new AllBeatmapsCollectionFilterMenuItem());
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c)));
if (ShowManageCollectionsItem)
filters.Add(new ManageCollectionsFilterMenuItem());
Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0];
}
/// <summary>
/// Occurs when the <see cref="CollectionFilterMenuItem"/> selection has changed.
/// </summary>
private void filterChanged(ValueChangedEvent<CollectionFilterMenuItem> filter)
{
// Binding the beatmaps will trigger a collection change event, which results in an infinite-loop. This is rebound later, when it's safe to do so.
beatmaps.CollectionChanged -= filterBeatmapsChanged;
if (filter.OldValue?.Collection != null)
beatmaps.UnbindFrom(filter.OldValue.Collection.BeatmapHashes);
if (filter.NewValue?.Collection != null)
beatmaps.BindTo(filter.NewValue.Collection.BeatmapHashes);
beatmaps.CollectionChanged += filterBeatmapsChanged;
// Never select the manage collection filter - rollback to the previous filter.
// This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value.
if (filter.NewValue is ManageCollectionsFilterMenuItem)
{
Current.Value = filter.OldValue;
manageCollectionsDialog?.Show();
}
}
/// <summary>
/// Occurs when the beatmaps contained by a <see cref="BeatmapCollection"/> have changed.
/// </summary>
private void filterBeatmapsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// The filtered beatmaps have changed, without the filter having changed itself. So a change in filter must be notified.
// Note that this does NOT propagate to bound bindables, so the FilterControl must bind directly to the value change event of this bindable.
Current.TriggerChange();
}
protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value;
protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d =>
{
d.SelectedItem.BindTarget = Current;
});
protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu();
protected virtual CollectionDropdownHeader CreateCollectionHeader() => new CollectionDropdownHeader();
protected virtual CollectionDropdownMenu CreateCollectionMenu() => new CollectionDropdownMenu();
public class CollectionDropdownHeader : OsuDropdownHeader
{
public readonly Bindable<CollectionFilterMenuItem> SelectedItem = new Bindable<CollectionFilterMenuItem>();
private readonly Bindable<string> collectionName = new Bindable<string>();
protected override LocalisableString Label
{
get => base.Label;
set { } // See updateText().
}
public CollectionDropdownHeader()
{
Height = 25;
Icon.Size = new Vector2(16);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 };
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedItem.BindValueChanged(_ => updateBindable(), true);
}
private void updateBindable()
{
collectionName.UnbindAll();
if (SelectedItem.Value != null)
collectionName.BindTo(SelectedItem.Value.CollectionName);
collectionName.BindValueChanged(_ => updateText(), true);
}
// Dropdowns don't bind to value changes, so the real name is copied directly from the selected item here.
private void updateText() => base.Label = collectionName.Value;
}
protected class CollectionDropdownMenu : OsuDropdownMenu
{
public CollectionDropdownMenu()
{
MaxHeight = 200;
}
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item)
{
BackgroundColourHover = HoverColour,
BackgroundColourSelected = SelectionColour
};
}
protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
{
[NotNull]
protected new CollectionFilterMenuItem Item => ((DropdownMenuItem<CollectionFilterMenuItem>)base.Item).Value;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
[CanBeNull]
private readonly BindableList<string> collectionBeatmaps;
[NotNull]
private readonly Bindable<string> collectionName;
private IconButton addOrRemoveButton;
private Content content;
private bool beatmapInCollection;
public CollectionDropdownMenuItem(MenuItem item)
: base(item)
{
collectionBeatmaps = Item.Collection?.BeatmapHashes.GetBoundCopy();
collectionName = Item.CollectionName.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(addOrRemoveButton = new IconButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
X = -OsuScrollContainer.SCROLL_BAR_HEIGHT,
Scale = new Vector2(0.65f),
Action = addOrRemove,
});
}
protected override void LoadComplete()
{
base.LoadComplete();
if (collectionBeatmaps != null)
{
collectionBeatmaps.CollectionChanged += (_, _) => collectionChanged();
beatmap.BindValueChanged(_ => collectionChanged(), true);
}
// Although the DrawableMenuItem binds to value changes of the item's text, the item is an internal implementation detail of Dropdown that has no knowledge
// of the underlying CollectionFilter value and its accompanying name, so the real name has to be copied here. Without this, the collection name wouldn't update when changed.
collectionName.BindValueChanged(name => content.Text = name.NewValue, true);
updateButtonVisibility();
}
protected override bool OnHover(HoverEvent e)
{
updateButtonVisibility();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateButtonVisibility();
base.OnHoverLost(e);
}
private void collectionChanged()
{
Debug.Assert(collectionBeatmaps != null);
beatmapInCollection = collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo.MD5Hash);
addOrRemoveButton.Enabled.Value = !beatmap.IsDefault;
addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare;
addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap";
updateButtonVisibility();
}
protected override void OnSelectChange()
{
base.OnSelectChange();
updateButtonVisibility();
}
private void updateButtonVisibility()
{
if (collectionBeatmaps == null)
addOrRemoveButton.Alpha = 0;
else
addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0;
}
private void addOrRemove()
{
Debug.Assert(collectionBeatmaps != null);
if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
collectionBeatmaps.Add(beatmap.Value.BeatmapInfo.MD5Hash);
}
protected override Drawable CreateContent() => content = (Content)base.CreateContent();
}
}
}

View File

@ -1,11 +1,8 @@
// 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 JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Game.Database;
namespace osu.Game.Collections
{
@ -18,26 +15,29 @@ namespace osu.Game.Collections
/// The collection to filter beatmaps from.
/// May be null to not filter by collection (include all beatmaps).
/// </summary>
[CanBeNull]
public readonly BeatmapCollection Collection;
public readonly Live<BeatmapCollection>? Collection;
/// <summary>
/// The name of the collection.
/// </summary>
[NotNull]
public readonly Bindable<string> CollectionName;
public string CollectionName { get; }
/// <summary>
/// Creates a new <see cref="CollectionFilterMenuItem"/>.
/// </summary>
/// <param name="collection">The collection to filter beatmaps from.</param>
public CollectionFilterMenuItem([CanBeNull] BeatmapCollection collection)
public CollectionFilterMenuItem(Live<BeatmapCollection> collection)
: this(collection.PerformRead(c => c.Name))
{
Collection = collection;
CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable<string>("All beatmaps");
}
public bool Equals(CollectionFilterMenuItem other)
protected CollectionFilterMenuItem(string name)
{
CollectionName = name;
}
public bool Equals(CollectionFilterMenuItem? other)
{
if (other == null)
return false;
@ -45,20 +45,20 @@ namespace osu.Game.Collections
// collections may have the same name, so compare first on reference equality.
// this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager.
if (Collection != null)
return Collection == other.Collection;
return Collection.ID == other.Collection?.ID;
// fallback to name-based comparison.
// this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below).
return CollectionName.Value == other.CollectionName.Value;
return CollectionName == other.CollectionName;
}
public override int GetHashCode() => CollectionName.Value.GetHashCode();
public override int GetHashCode() => CollectionName.GetHashCode();
}
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem
{
public AllBeatmapsCollectionFilterMenuItem()
: base(null)
: base("All beatmaps")
{
}
}
@ -66,9 +66,8 @@ namespace osu.Game.Collections
public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem
{
public ManageCollectionsFilterMenuItem()
: base(null)
: base("Manage collections...")
{
CollectionName.Value = "Manage collections...";
}
}
}

View File

@ -1,349 +0,0 @@
// 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.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Legacy;
using osu.Game.Overlays.Notifications;
namespace osu.Game.Collections
{
/// <summary>
/// Handles user-defined collections of beatmaps.
/// </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
{
/// <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>();
private readonly Storage storage;
public CollectionManager(Storage storage)
{
this.storage = storage;
}
[Resolved(canBeNull: true)]
private DatabaseContextFactory efContextFactory { get; set; } = null!;
[BackgroundDependencyLoader]
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(() =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var c in e.NewItems.Cast<BeatmapCollection>())
c.Changed += backgroundSave;
break;
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 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()
{
Collections.Clear();
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();
}
}
}

View File

@ -2,22 +2,26 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Collections
{
public class CollectionToggleMenuItem : ToggleMenuItem
{
public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap)
: base(collection.Name.Value, MenuItemType.Standard, state =>
public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap)
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state =>
{
if (state)
collection.BeatmapHashes.Add(beatmap.MD5Hash);
else
collection.BeatmapHashes.Remove(beatmap.MD5Hash);
collection.PerformWrite(c =>
{
if (state)
c.BeatmapMD5Hashes.Add(beatmap.MD5Hash);
else
c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash);
});
})
{
State.Value = collection.BeatmapHashes.Contains(beatmap.MD5Hash);
State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash));
}
}
}

View File

@ -1,21 +1,20 @@
// 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 Humanizer;
using osu.Framework.Graphics.Sprites;
using osu.Game.Database;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Collections
{
public class DeleteCollectionDialog : PopupDialog
{
public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction)
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
{
HeaderText = "Confirm deletion of";
BodyText = $"{collection.Name.Value} ({"beatmap".ToQuantity(collection.BeatmapHashes.Count)})";
BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})");
Icon = FontAwesome.Regular.TrashAlt;

View File

@ -1,39 +1,66 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osuTK;
using Realms;
namespace osu.Game.Collections
{
/// <summary>
/// Visualises a list of <see cref="BeatmapCollection"/>s.
/// </summary>
public class DrawableCollectionList : OsuRearrangeableListContainer<BeatmapCollection>
public class DrawableCollectionList : OsuRearrangeableListContainer<Live<BeatmapCollection>>
{
private Scroll scroll;
protected override ScrollContainer<Drawable> CreateScrollContainer() => scroll = new Scroll();
protected override FillFlowContainer<RearrangeableListItem<BeatmapCollection>> CreateListFillFlowContainer() => new Flow
[Resolved]
private RealmAccess realm { get; set; } = null!;
private Scroll scroll = null!;
private IDisposable? realmSubscription;
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>> CreateListFillFlowContainer() => new Flow
{
DragActive = { BindTarget = DragActive }
};
protected override OsuRearrangeableListItem<BeatmapCollection> CreateOsuDrawable(BeatmapCollection item)
protected override void LoadComplete()
{
if (item == scroll.PlaceholderItem.Model)
base.LoadComplete();
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
}
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes, Exception error)
{
Items.Clear();
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
}
protected override OsuRearrangeableListItem<Live<BeatmapCollection>> CreateOsuDrawable(Live<BeatmapCollection> item)
{
if (item.ID == scroll.PlaceholderItem.Model.ID)
return scroll.ReplacePlaceholder();
return new DrawableCollectionListItem(item, true);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
realmSubscription?.Dispose();
}
/// <summary>
/// The scroll container for this <see cref="DrawableCollectionList"/>.
/// Contains the main flow of <see cref="DrawableCollectionListItem"/> and attaches a placeholder item to the end of the list.
@ -46,7 +73,7 @@ namespace osu.Game.Collections
/// <summary>
/// The currently-displayed placeholder item.
/// </summary>
public DrawableCollectionListItem PlaceholderItem { get; private set; }
public DrawableCollectionListItem PlaceholderItem { get; private set; } = null!;
protected override Container<Drawable> Content => content;
private readonly Container content;
@ -76,6 +103,7 @@ namespace osu.Game.Collections
});
ReplacePlaceholder();
Debug.Assert(PlaceholderItem != null);
}
protected override void Update()
@ -95,7 +123,7 @@ namespace osu.Game.Collections
var previous = PlaceholderItem;
placeholderContainer.Clear(false);
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection(), false));
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection().ToLiveUnmanaged(), false));
return previous;
}
@ -104,7 +132,7 @@ namespace osu.Game.Collections
/// <summary>
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
/// </summary>
private class Flow : FillFlowContainer<RearrangeableListItem<BeatmapCollection>>
private class Flow : FillFlowContainer<RearrangeableListItem<Live<BeatmapCollection>>>
{
public readonly IBindable<bool> DragActive = new Bindable<bool>();

View File

@ -1,17 +1,16 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@ -24,79 +23,62 @@ namespace osu.Game.Collections
/// <summary>
/// Visualises a <see cref="BeatmapCollection"/> inside a <see cref="DrawableCollectionList"/>.
/// </summary>
public class DrawableCollectionListItem : OsuRearrangeableListItem<BeatmapCollection>
public class DrawableCollectionListItem : OsuRearrangeableListItem<Live<BeatmapCollection>>
{
private const float item_height = 35;
private const float button_width = item_height * 0.75f;
/// <summary>
/// Whether the <see cref="BeatmapCollection"/> currently exists inside the <see cref="CollectionManager"/>.
/// </summary>
public IBindable<bool> IsCreated => isCreated;
private readonly Bindable<bool> isCreated = new Bindable<bool>();
/// <summary>
/// Creates a new <see cref="DrawableCollectionListItem"/>.
/// </summary>
/// <param name="item">The <see cref="BeatmapCollection"/>.</param>
/// <param name="isCreated">Whether <paramref name="item"/> currently exists inside the <see cref="CollectionManager"/>.</param>
public DrawableCollectionListItem(BeatmapCollection item, bool isCreated)
/// <param name="isCreated">Whether <paramref name="item"/> currently exists inside realm.</param>
public DrawableCollectionListItem(Live<BeatmapCollection> item, bool isCreated)
: base(item)
{
this.isCreated.Value = isCreated;
ShowDragHandle.BindTo(this.isCreated);
ShowDragHandle.Value = item.IsManaged;
}
protected override Drawable CreateContent() => new ItemContent(Model)
{
IsCreated = { BindTarget = isCreated }
};
protected override Drawable CreateContent() => new ItemContent(Model);
/// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>.
/// </summary>
private class ItemContent : CircularContainer
{
public readonly Bindable<bool> IsCreated = new Bindable<bool>();
private readonly Live<BeatmapCollection> collection;
private readonly IBindable<string> collectionName;
private readonly BeatmapCollection collection;
private ItemTextBox textBox = null!;
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
[Resolved]
private RealmAccess realm { get; set; } = null!;
private Container textBoxPaddingContainer;
private ItemTextBox textBox;
public ItemContent(BeatmapCollection collection)
public ItemContent(Live<BeatmapCollection> collection)
{
this.collection = collection;
RelativeSizeAxes = Axes.X;
Height = item_height;
Masking = true;
collectionName = collection.Name.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
Children = new[]
{
new DeleteButton(collection)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
IsCreated = { BindTarget = IsCreated },
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
},
textBoxPaddingContainer = new Container
collection.IsManaged
? new DeleteButton(collection)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
}
: Empty(),
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = button_width },
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
Children = new Drawable[]
{
textBox = new ItemTextBox
@ -104,7 +86,7 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
CornerRadius = item_height / 2,
PlaceholderText = IsCreated.Value ? string.Empty : "Create a new collection"
PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection"
},
}
},
@ -116,28 +98,18 @@ namespace osu.Game.Collections
base.LoadComplete();
// Bind late, as the collection name may change externally while still loading.
textBox.Current = collection.Name;
collectionName.BindValueChanged(_ => createNewCollection(), true);
IsCreated.BindValueChanged(created => textBoxPaddingContainer.Padding = new MarginPadding { Right = created.NewValue ? button_width : 0 }, true);
textBox.Current.Value = collection.PerformRead(c => c.IsValid ? c.Name : string.Empty);
textBox.OnCommit += onCommit;
}
private void createNewCollection()
private void onCommit(TextBox sender, bool newText)
{
if (IsCreated.Value)
return;
if (collection.IsManaged)
collection.PerformWrite(c => c.Name = textBox.Current.Value);
else if (!string.IsNullOrEmpty(textBox.Current.Value))
realm.Write(r => r.Add(new BeatmapCollection(textBox.Current.Value)));
if (string.IsNullOrEmpty(collectionName.Value))
return;
// Add the new collection and disable our placeholder. If all text is removed, the placeholder should not show back again.
collectionManager?.Collections.Add(collection);
textBox.PlaceholderText = string.Empty;
// When this item changes from placeholder to non-placeholder (via changing containers), its textbox will lose focus, so it needs to be re-focused.
Schedule(() => GetContainingInputManager().ChangeFocus(textBox));
IsCreated.Value = true;
textBox.Text = string.Empty;
}
}
@ -155,22 +127,17 @@ namespace osu.Game.Collections
public class DeleteButton : CompositeDrawable
{
public readonly IBindable<bool> IsCreated = new Bindable<bool>();
public Func<Vector2, bool> IsTextBoxHovered = null!;
public Func<Vector2, bool> IsTextBoxHovered;
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved(CanBeNull = true)]
private IDialogOverlay dialogOverlay { get; set; }
private readonly Live<BeatmapCollection> collection;
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
private Drawable fadeContainer = null!;
private Drawable background = null!;
private readonly BeatmapCollection collection;
private Drawable fadeContainer;
private Drawable background;
public DeleteButton(BeatmapCollection collection)
public DeleteButton(Live<BeatmapCollection> collection)
{
this.collection = collection;
RelativeSizeAxes = Axes.Y;
@ -204,12 +171,6 @@ namespace osu.Game.Collections
};
}
protected override void LoadComplete()
{
base.LoadComplete();
IsCreated.BindValueChanged(created => Alpha = created.NewValue ? 1 : 0, true);
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos);
protected override bool OnHover(HoverEvent e)
@ -227,7 +188,7 @@ namespace osu.Game.Collections
{
background.FlashColour(Color4.White, 150);
if (collection.BeatmapHashes.Count == 0)
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
@ -235,7 +196,7 @@ namespace osu.Game.Collections
return true;
}
private void deleteCollection() => collectionManager?.Collections.Remove(collection);
private void deleteCollection() => collection.PerformWrite(c => c.Realm.Remove(c));
}
}
}

View File

@ -1,11 +1,8 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -24,10 +21,7 @@ namespace osu.Game.Collections
private const double enter_duration = 500;
private const double exit_duration = 200;
private AudioFilter lowPassFilter;
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
private AudioFilter lowPassFilter = null!;
public ManageCollectionsDialog()
{
@ -107,7 +101,6 @@ namespace osu.Game.Collections
new DrawableCollectionList
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = collectionManager?.Collections ?? new BindableList<BeatmapCollection>() }
}
}
}

View File

@ -0,0 +1,169 @@
// 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;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Collections;
using osu.Game.IO.Legacy;
using osu.Game.Overlays.Notifications;
namespace osu.Game.Database
{
public class LegacyCollectionImporter
{
public Action<Notification>? PostNotification { protected get; set; }
private readonly RealmAccess realm;
private const string database_name = "collection.db";
public LegacyCollectionImporter(RealmAccess realm)
{
this.realm = realm;
}
public Task<int> GetAvailableCount(Storage storage)
{
if (!storage.Exists(database_name))
return Task.FromResult(0);
return Task.Run(() =>
{
using (var stream = storage.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 ImportFromStorage(Storage storage)
{
if (!storage.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 = storage.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>();
try
{
realm.Write(r =>
{
foreach (var collection in newCollections)
{
var existing = r.All<BeatmapCollection>().FirstOrDefault(c => c.Name == collection.Name);
if (existing != null)
{
foreach (string newBeatmap in existing.BeatmapMD5Hashes)
{
if (!existing.BeatmapMD5Hashes.Contains(newBeatmap))
existing.BeatmapMD5Hashes.Add(newBeatmap);
}
}
else
r.Add(collection);
}
});
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(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.BeatmapMD5Hashes.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;
}
}
}

View File

@ -13,7 +13,6 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.IO;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings.Sections.Maintenance;
@ -36,15 +35,15 @@ namespace osu.Game.Database
[Resolved]
private ScoreManager scores { get; set; }
[Resolved]
private CollectionManager collections { get; set; }
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
[Resolved]
private IDialogOverlay dialogOverlay { get; set; }
[Resolved]
private RealmAccess realmAccess { get; set; }
[Resolved(canBeNull: true)]
private DesktopGameHost desktopGameHost { get; set; }
@ -72,7 +71,7 @@ namespace osu.Game.Database
return await new LegacySkinImporter(skins).GetAvailableCount(stableStorage);
case StableContent.Collections:
return await collections.GetAvailableCount(stableStorage);
return await new LegacyCollectionImporter(realmAccess).GetAvailableCount(stableStorage);
case StableContent.Scores:
return await new LegacyScoreImporter(scores).GetAvailableCount(stableStorage);
@ -109,7 +108,7 @@ namespace osu.Game.Database
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
if (content.HasFlagFast(StableContent.Collections))
importTasks.Add(beatmapImportTask.ContinueWith(_ => collections.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
if (content.HasFlagFast(StableContent.Scores))
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
namespace osu.Game.Database
{
@ -18,19 +19,19 @@ namespace osu.Game.Database
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public abstract void PerformRead(Action<T> perform);
public abstract void PerformRead([InstantHandle] Action<T> perform);
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public abstract TReturn PerformRead<TReturn>(Func<T, TReturn> perform);
public abstract TReturn PerformRead<TReturn>([InstantHandle] Func<T, TReturn> perform);
/// <summary>
/// Perform a write operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public abstract void PerformWrite(Action<T> perform);
public abstract void PerformWrite([InstantHandle] Action<T> perform);
/// <summary>
/// Whether this instance is tracking data which is managed by the database backing.

View File

@ -14,6 +14,7 @@ using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -64,8 +65,9 @@ namespace osu.Game.Database
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
/// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo.
/// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1.
/// 21 2022-07-27 Migrate collections to realm (BeatmapCollection).
/// </summary>
private const int schema_version = 20;
private const int schema_version = 21;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -790,6 +792,27 @@ namespace osu.Game.Database
beatmap.StarRating = -1;
break;
case 21:
try
{
// Migrate collections from external file to inside realm.
// We use the "legacy" importer because that is how things were actually being saved out until now.
var legacyCollectionImporter = new LegacyCollectionImporter(this);
if (legacyCollectionImporter.GetAvailableCount(storage).GetResultSafely() > 0)
{
legacyCollectionImporter.ImportFromStorage(storage);
storage.Delete("collection.db");
}
}
catch (Exception e)
{
// can be removed 20221027 (just for initial safety).
Logger.Error(e, "Collections could not be migrated to realm. Please provide your \"collection.db\" to the dev team.");
}
break;
}
}

View File

@ -80,7 +80,7 @@ namespace osu.Game.Online.Spectator
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(ISpectatorServer.SendFrameData), bundle);
return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), bundle);
}
protected override Task EndPlayingInternal(SpectatorState state)

View File

@ -304,7 +304,7 @@ namespace osu.Game.Online.Spectator
SendFramesInternal(bundle).ContinueWith(t =>
{
// Handle exception outside of `Schedule` to ensure it doesn't go unovserved.
// Handle exception outside of `Schedule` to ensure it doesn't go unobserved.
bool wasSuccessful = t.Exception == null;
return Schedule(() =>

View File

@ -858,11 +858,6 @@ namespace osu.Game
d.Origin = Anchor.TopRight;
}), rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(new CollectionManager(Storage)
{
PostNotification = n => Notifications.Post(n),
}, Add, true);
loadComponentSingleFile(legacyImportManager, Add);
loadComponentSingleFile(screenshotManager, Add);

View File

@ -271,7 +271,7 @@ namespace osu.Game
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, API, difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));

View File

@ -161,7 +161,6 @@ namespace osu.Game.Overlays.FirstRunSetup
private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets)
{
Beatmap.Value = new DummyWorkingBeatmap(audio, textures);
Beatmap.Value.LoadTrack();
Ruleset.Value = rulesets.AvailableRulesets.First();

View File

@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Music
public Action<FilterCriteria> FilterChanged;
public readonly FilterTextBox Search;
private readonly CollectionDropdown collectionDropdown;
private readonly NowPlayingCollectionDropdown collectionDropdown;
public FilterControl()
{
@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Music
RelativeSizeAxes = Axes.X,
Height = 40,
},
collectionDropdown = new CollectionDropdown { RelativeSizeAxes = Axes.X }
collectionDropdown = new NowPlayingCollectionDropdown { RelativeSizeAxes = Axes.X }
},
},
};

View File

@ -5,6 +5,7 @@
using JetBrains.Annotations;
using osu.Game.Collections;
using osu.Game.Database;
namespace osu.Game.Overlays.Music
{
@ -19,6 +20,6 @@ namespace osu.Game.Overlays.Music
/// The collection to filter beatmaps from.
/// </summary>
[CanBeNull]
public BeatmapCollection Collection;
public Live<BeatmapCollection> Collection;
}
}

View File

@ -15,9 +15,9 @@ using osu.Game.Graphics;
namespace osu.Game.Overlays.Music
{
/// <summary>
/// A <see cref="CollectionFilterDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
/// A <see cref="CollectionDropdown"/> for use in the <see cref="NowPlayingOverlay"/>.
/// </summary>
public class CollectionDropdown : CollectionFilterDropdown
public class NowPlayingCollectionDropdown : CollectionDropdown
{
protected override bool ShowManageCollectionsItem => false;

View File

@ -31,14 +31,16 @@ namespace osu.Game.Overlays.Music
{
var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer;
string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray());
foreach (var item in items.OfType<PlaylistItem>())
{
if (criteria.Collection == null)
if (currentCollectionHashes == null)
item.InSelectedCollection = true;
else
{
item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash)
.Any(criteria.Collection.BeatmapHashes.Contains);
.Any(currentCollectionHashes.Contains);
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Localisation;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
@ -15,11 +16,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private SettingsButton importCollectionsButton = null!;
[BackgroundDependencyLoader]
private void load(CollectionManager? collectionManager, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
{
if (collectionManager == null) return;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private INotificationOverlay? notificationOverlay { get; set; }
[BackgroundDependencyLoader]
private void load(LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
{
if (legacyImportManager?.SupportsImportFromStable == true)
{
Add(importCollectionsButton = new SettingsButton
@ -38,9 +43,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Text = MaintenanceSettingsStrings.DeleteAllCollections,
Action = () =>
{
dialogOverlay?.Push(new MassDeleteConfirmationDialog(collectionManager.DeleteAll));
dialogOverlay?.Push(new MassDeleteConfirmationDialog(deleteAllCollections));
}
});
}
private void deleteAllCollections()
{
realm.Write(r => r.RemoveAll<BeatmapCollection>());
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" });
}
}
}

View File

@ -94,6 +94,9 @@ namespace osu.Game.Screens.OnlinePlay
private PanelBackground panelBackground;
private FillFlowContainer mainFillFlow;
[Resolved]
private RealmAccess realm { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
@ -112,9 +115,6 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(CanBeNull = true)]
private BeatmapSetOverlay beatmapOverlay { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
@ -495,11 +495,11 @@ namespace osu.Game.Screens.OnlinePlay
if (beatmapOverlay != null)
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(Item.Beatmap.OnlineID)));
if (collectionManager != null && beatmap != null)
if (beatmap != null)
{
if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending)
{
var collectionItems = collectionManager.Collections.Select(c => new CollectionToggleMenuItem(c, beatmap)).Cast<OsuMenuItem>().ToList();
var collectionItems = realm.Realm.All<BeatmapCollection>().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast<OsuMenuItem>().ToList();
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));

View File

@ -76,7 +76,7 @@ namespace osu.Game.Screens.Select.Carousel
}
if (match)
match &= criteria.Collection?.BeatmapHashes.Contains(BeatmapInfo.MD5Hash) ?? true;
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
if (match && criteria.RulesetCriteria != null)
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);

View File

@ -22,6 +22,7 @@ using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
@ -63,12 +64,12 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
[Resolved]
private RealmAccess realm { get; set; }
private IBindable<StarDifficulty?> starDifficultyBindable;
private CancellationTokenSource starDifficultyCancellationSource;
@ -237,14 +238,11 @@ namespace osu.Game.Screens.Select.Carousel
if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null)
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID)));
if (collectionManager != null)
{
var collectionItems = collectionManager.Collections.Select(c => new CollectionToggleMenuItem(c, beatmapInfo)).Cast<OsuMenuItem>().ToList();
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
var collectionItems = realm.Realm.All<BeatmapCollection>().AsEnumerable().Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast<OsuMenuItem>().ToList();
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
}
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
if (hideRequested != null)
items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo)));

View File

@ -17,6 +17,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
@ -32,12 +33,12 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved(CanBeNull = true)]
private IDialogOverlay dialogOverlay { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
[Resolved]
private RealmAccess realm { get; set; }
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
[CanBeNull]
@ -223,14 +224,11 @@ namespace osu.Game.Screens.Select.Carousel
if (beatmapSet.OnlineID > 0 && viewDetails != null)
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID)));
if (collectionManager != null)
{
var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList();
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
var collectionItems = realm.Realm.All<BeatmapCollection>().AsEnumerable().Select(createCollectionMenuItem).ToList();
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
}
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet)));
@ -247,7 +245,7 @@ namespace osu.Game.Screens.Select.Carousel
TernaryState state;
int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapHashes.Contains(b.MD5Hash));
int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapMD5Hashes.Contains(b.MD5Hash));
if (countExisting == beatmapSet.Beatmaps.Count)
state = TernaryState.True;
@ -256,24 +254,29 @@ namespace osu.Game.Screens.Select.Carousel
else
state = TernaryState.False;
return new TernaryStateToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
var liveCollection = collection.ToLive(realm);
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
{
foreach (var b in beatmapSet.Beatmaps)
liveCollection.PerformWrite(c =>
{
switch (s)
foreach (var b in beatmapSet.Beatmaps)
{
case TernaryState.True:
if (collection.BeatmapHashes.Contains(b.MD5Hash))
continue;
switch (s)
{
case TernaryState.True:
if (c.BeatmapMD5Hashes.Contains(b.MD5Hash))
continue;
collection.BeatmapHashes.Add(b.MD5Hash);
break;
c.BeatmapMD5Hashes.Add(b.MD5Hash);
break;
case TernaryState.False:
collection.BeatmapHashes.Remove(b.MD5Hash);
break;
case TernaryState.False:
c.BeatmapMD5Hashes.Remove(b.MD5Hash);
break;
}
}
}
});
})
{
State = { Value = state }

View File

@ -39,6 +39,10 @@ namespace osu.Game.Screens.Select
private Bindable<GroupMode> groupMode;
private SeekLimitedSearchTextBox searchTextBox;
private CollectionDropdown collectionDropdown;
public FilterCriteria CreateCriteria()
{
string query = searchTextBox.Text;
@ -49,7 +53,7 @@ namespace osu.Game.Screens.Select
Sort = sortMode.Value,
AllowConvertedBeatmaps = showConverted.Value,
Ruleset = ruleset.Value,
Collection = collectionDropdown?.Current.Value?.Collection
CollectionBeatmapMD5Hashes = collectionDropdown.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes)
};
if (!minimumStars.IsDefault)
@ -64,10 +68,6 @@ namespace osu.Game.Screens.Select
return criteria;
}
private SeekLimitedSearchTextBox searchTextBox;
private CollectionFilterDropdown collectionDropdown;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos);
@ -179,10 +179,11 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.Both,
Width = 0.48f,
},
collectionDropdown = new CollectionFilterDropdown
collectionDropdown = new CollectionDropdown
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RequestFilter = updateCriteria,
RelativeSizeAxes = Axes.X,
Y = 4,
Width = 0.5f,
@ -209,15 +210,6 @@ namespace osu.Game.Screens.Select
groupMode.BindValueChanged(_ => updateCriteria());
sortMode.BindValueChanged(_ => updateCriteria());
collectionDropdown.Current.ValueChanged += val =>
{
if (val.NewValue == null)
// may be null briefly while menu is repopulated.
return;
updateCriteria();
};
searchTextBox.Current.ValueChanged += _ => updateCriteria();
updateCriteria();

View File

@ -68,10 +68,10 @@ namespace osu.Game.Screens.Select
}
/// <summary>
/// The collection to filter beatmaps from.
/// Hashes from the <see cref="BeatmapCollection"/> to filter to.
/// </summary>
[CanBeNull]
public BeatmapCollection Collection;
public IEnumerable<string> CollectionBeatmapMD5Hashes { get; set; }
[CanBeNull]
public IRulesetFilterCriteria RulesetCriteria { get; set; }

View File

@ -31,8 +31,6 @@ namespace osu.Game.Tests.Beatmaps
this.storyboard = storyboard;
}
public override bool TrackLoaded => true;
public override bool BeatmapLoaded => true;
protected override IBeatmap GetBeatmap() => beatmap;

View File

@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual
public WorkingBeatmap TestBeatmap;
public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host, WorkingBeatmap defaultBeatmap)
: base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap)
: base(storage, realm, api, audioManager, resources, host, defaultBeatmap)
{
}

View File

@ -3,7 +3,6 @@
#nullable disable
using NUnit.Framework;
using osu.Game.Online.Rooms;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
@ -34,13 +33,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
this.joinRoom = joinRoom;
}
[SetUp]
public new void Setup() => Schedule(() =>
{
if (joinRoom)
SelectedRoom.Value = CreateRoom();
});
protected virtual Room CreateRoom()
{
return new Room
@ -63,7 +55,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
if (joinRoom)
{
AddStep("join room", () => RoomManager.CreateRoom(SelectedRoom.Value));
AddStep("join room", () =>
{
SelectedRoom.Value = CreateRoom();
RoomManager.CreateRoom(SelectedRoom.Value);
});
AddUntilStep("wait for room join", () => RoomJoined);
}
}

View File

@ -4,7 +4,6 @@
#nullable disable
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -56,39 +55,43 @@ namespace osu.Game.Tests.Visual.OnlinePlay
return dependencies;
}
[SetUp]
public void Setup() => Schedule(() =>
public override void SetUpSteps()
{
// Reset the room dependencies to a fresh state.
drawableDependenciesContainer.Clear();
dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies();
drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents);
base.SetUpSteps();
var handler = OnlinePlayDependencies.RequestsHandler;
// Resolving the BeatmapManager in the test scene will inject the game-wide BeatmapManager, while many test scenes cache their own BeatmapManager instead.
// To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead.
var beatmapManager = dependencies.Get<BeatmapManager>();
((DummyAPIAccess)API).HandleRequest = request =>
AddStep("setup dependencies", () =>
{
try
// Reset the room dependencies to a fresh state.
drawableDependenciesContainer.Clear();
dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies();
drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents);
var handler = OnlinePlayDependencies.RequestsHandler;
// Resolving the BeatmapManager in the test scene will inject the game-wide BeatmapManager, while many test scenes cache their own BeatmapManager instead.
// To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead.
var beatmapManager = dependencies.Get<BeatmapManager>();
((DummyAPIAccess)API).HandleRequest = request =>
{
return handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
}
catch (ObjectDisposedException)
{
// These requests can be fired asynchronously, but potentially arrive after game components
// have been disposed (ie. realm in BeatmapManager).
// This only happens in tests and it's easiest to ignore them for now.
Logger.Log($"Handled {nameof(ObjectDisposedException)} in test request handling");
return true;
}
};
});
try
{
return handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
}
catch (ObjectDisposedException)
{
// These requests can be fired asynchronously, but potentially arrive after game components
// have been disposed (ie. realm in BeatmapManager).
// This only happens in tests and it's easiest to ignore them for now.
Logger.Log($"Handled {nameof(ObjectDisposedException)} in test request handling");
return true;
}
};
});
}
/// <summary>
/// Creates the room dependencies. Called every <see cref="Setup"/>.
/// Creates the room dependencies. Called every <see cref="SetUpSteps"/>.
/// </summary>
/// <remarks>
/// Any custom dependencies required for online play sub-classes should be added here.

View File

@ -365,6 +365,11 @@ namespace osu.Game.Tests.Visual
}
else
track = audio?.Tracks.GetVirtual(trackLength);
// We are guaranteed to have a virtual track.
// To ease testability, ensure the track is available from point of construction.
// (Usually this would be done by MusicController for us).
LoadTrack();
}
~ClockBackedTestWorkingBeatmap()