diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cac1bf05a6..6e2b9d20a8 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -1050,7 +1050,7 @@ namespace osu.Game.Tests.Beatmaps.IO private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected) { - Assert.AreEqual(expected, osu.Dependencies.Get().QueryFiles(f => f.ReferenceCount == 1).Count()); + Assert.AreEqual(expected, osu.Dependencies.Get().Get().FileInfo.Count(f => f.ReferenceCount == 1)); } private static void ensureLoaded(OsuGameBase osu, int timeout = 60000) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 33aa1afb89..f86761fdc8 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using NUnit.Framework; -using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Models; using Realms; @@ -18,18 +17,35 @@ namespace osu.Game.Tests.Database public class RealmLiveTests : RealmTest { [Test] - public void TestLiveCastability() + public void TestLiveEquality() { RunTestWithRealm((realmFactory, _) => { - RealmLive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(); + ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(); - ILive iBeatmap = beatmap; + ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(); - Assert.AreEqual(0, iBeatmap.Value.Length); + Assert.AreEqual(beatmap, beatmap2); }); } + [Test] + public void TestAccessNonManaged() + { + var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + var liveBeatmap = beatmap.ToLive(); + + Assert.IsFalse(beatmap.Hidden); + Assert.IsFalse(liveBeatmap.Value.Hidden); + Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); + + Assert.Throws(() => liveBeatmap.PerformWrite(l => l.Hidden = true)); + + Assert.IsFalse(beatmap.Hidden); + Assert.IsFalse(liveBeatmap.Value.Hidden); + Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); + } + [Test] public void TestValueAccessWithOpenContext() { diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs index f3a4f10210..f9b7bfa586 100644 --- a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.IO; using System.Linq; using Moq; @@ -13,7 +12,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; -using FileInfo = osu.Game.IO.FileInfo; namespace osu.Game.Tests.Editing.Checks { @@ -33,14 +31,10 @@ namespace osu.Game.Tests.Editing.Checks { BeatmapSet = new BeatmapSetInfo { - Files = new List(new[] + Files = { - new BeatmapSetFileInfo - { - Filename = "abc123.mp4", - FileInfo = new FileInfo { Hash = "abcdef" } - } - }) + CheckTestHelpers.CreateMockFile("mp4"), + } } } }; diff --git a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs index 05bfae7e63..bb560054a3 100644 --- a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; +using System; using System.IO; using System.Linq; using JetBrains.Annotations; @@ -12,7 +12,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; -using FileInfo = osu.Game.IO.FileInfo; namespace osu.Game.Tests.Editing.Checks { @@ -25,25 +24,17 @@ namespace osu.Game.Tests.Editing.Checks [SetUp] public void Setup() { + var file = CheckTestHelpers.CreateMockFile("jpg"); + check = new CheckBackgroundQuality(); beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { - Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" }, + Metadata = new BeatmapMetadata { BackgroundFile = file.Filename }, BeatmapSet = new BeatmapSetInfo { - Files = new List(new[] - { - new BeatmapSetFileInfo - { - Filename = "abc123.jpg", - FileInfo = new FileInfo - { - Hash = "abcdef" - } - } - }) + Files = { file } } } }; @@ -54,7 +45,7 @@ namespace osu.Game.Tests.Editing.Checks { // While this is a problem, it is out of scope for this check and is caught by a different one. beatmap.Metadata.BackgroundFile = string.Empty; - var context = getContext(null, new MemoryStream(System.Array.Empty())); + var context = getContext(null, new MemoryStream(Array.Empty())); Assert.That(check.Run(context), Is.Empty); } diff --git a/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs b/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs index 70e4c76b19..f36454aa71 100644 --- a/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs @@ -1,11 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.IO; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; @@ -22,22 +20,17 @@ namespace osu.Game.Tests.Editing.Checks [SetUp] public void Setup() { + var file = CheckTestHelpers.CreateMockFile("jpg"); + check = new CheckBackgroundPresence(); beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { - Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" }, + Metadata = new BeatmapMetadata { BackgroundFile = file.Filename }, BeatmapSet = new BeatmapSetInfo { - Files = new List(new[] - { - new BeatmapSetFileInfo - { - Filename = "abc123.jpg", - FileInfo = new FileInfo { Hash = "abcdef" } - } - }) + Files = { file } } } }; diff --git a/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs b/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs new file mode 100644 index 0000000000..f702921986 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.IO; + +namespace osu.Game.Tests.Editing.Checks +{ + public static class CheckTestHelpers + { + public static BeatmapSetFileInfo CreateMockFile(string extension) => + new BeatmapSetFileInfo + { + Filename = $"abc123.{extension}", + FileInfo = new FileInfo { Hash = "abcdef" } + }; + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs index 9b090591bc..8adf0d3764 100644 --- a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.IO; using System.Linq; using ManagedBass; @@ -14,7 +13,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osuTK.Audio; -using FileInfo = osu.Game.IO.FileInfo; namespace osu.Game.Tests.Editing.Checks { @@ -34,14 +32,7 @@ namespace osu.Game.Tests.Editing.Checks { BeatmapSet = new BeatmapSetInfo { - Files = new List(new[] - { - new BeatmapSetFileInfo - { - Filename = "abc123.wav", - FileInfo = new FileInfo { Hash = "abcdef" } - } - }) + Files = { CheckTestHelpers.CreateMockFile("wav") } } } }; @@ -55,11 +46,7 @@ namespace osu.Game.Tests.Editing.Checks public void TestDifferentExtension() { beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); - beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo - { - Filename = "abc123.jpg", - FileInfo = new FileInfo { Hash = "abcdef" } - }); + beatmap.BeatmapInfo.BeatmapSet.Files.Add(CheckTestHelpers.CreateMockFile("jpg")); // Should fail to load, but not produce an error due to the extension not being expected to load. Assert.IsEmpty(check.Run(getContext(null, allowMissing: true))); diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs index c9adc030c1..79d00e6a60 100644 --- a/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.IO; using System.Linq; using Moq; @@ -10,7 +9,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Objects; -using FileInfo = osu.Game.IO.FileInfo; namespace osu.Game.Tests.Editing.Checks { @@ -30,14 +28,10 @@ namespace osu.Game.Tests.Editing.Checks { BeatmapSet = new BeatmapSetInfo { - Files = new List(new[] + Files = { - new BeatmapSetFileInfo - { - Filename = "abc123.jpg", - FileInfo = new FileInfo { Hash = "abcdef" } - } - }) + CheckTestHelpers.CreateMockFile("jpg"), + } } } }; diff --git a/osu.Game.Tests/Online/TestSceneBeatmapManager.cs b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs similarity index 94% rename from osu.Game.Tests/Online/TestSceneBeatmapManager.cs rename to osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs index fc1b4f224d..4e77973655 100644 --- a/osu.Game.Tests/Online/TestSceneBeatmapManager.cs +++ b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs @@ -12,9 +12,9 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Online { [HeadlessTest] - public class TestSceneBeatmapManager : OsuTestScene + public class TestSceneBeatmapDownloading : OsuTestScene { - private BeatmapManager beatmaps; + private BeatmapModelDownloader beatmaps; private ProgressNotification recentNotification; private static readonly BeatmapSetInfo test_db_model = new BeatmapSetInfo @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Online }; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps) + private void load(BeatmapModelDownloader beatmaps) { this.beatmaps = beatmaps; diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index a4c69075be..24824b1e23 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -33,6 +33,7 @@ namespace osu.Game.Tests.Online { private RulesetStore rulesets; private TestBeatmapManager beatmaps; + private TestBeatmapModelDownloader beatmapDownloader; private string testBeatmapFile; private BeatmapInfo testBeatmapInfo; @@ -46,6 +47,7 @@ namespace osu.Game.Tests.Online { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.CacheAs(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API, host)); } [SetUp] @@ -80,13 +82,13 @@ namespace osu.Game.Tests.Online AddAssert("ensure beatmap unavailable", () => !beatmaps.IsAvailableLocally(testBeatmapSet)); addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); - AddStep("start downloading", () => beatmaps.Download(testBeatmapSet)); + AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet)); addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0f)); - AddStep("set progress 40%", () => ((TestDownloadRequest)beatmaps.GetExistingDownload(testBeatmapSet)).SetProgress(0.4f)); + AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)).SetProgress(0.4f)); addAvailabilityCheckStep("state downloading 40%", () => BeatmapAvailability.Downloading(0.4f)); - AddStep("finish download", () => ((TestDownloadRequest)beatmaps.GetExistingDownload(testBeatmapSet)).TriggerSuccess(testBeatmapFile)); + AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)).TriggerSuccess(testBeatmapFile)); addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); @@ -171,22 +173,6 @@ namespace osu.Game.Tests.Online return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); } - protected override BeatmapModelDownloader CreateBeatmapModelDownloader(IModelImporter manager, IAPIProvider api, GameHost host) - { - return new TestBeatmapModelDownloader(manager, api, host); - } - - internal class TestBeatmapModelDownloader : BeatmapModelDownloader - { - public TestBeatmapModelDownloader(IModelImporter importer, IAPIProvider apiProvider, GameHost gameHost) - : base(importer, apiProvider, gameHost) - { - } - - protected override ArchiveDownloadRequest CreateDownloadRequest(IBeatmapSetInfo set, bool minimiseDownloadSize) - => new TestDownloadRequest(set); - } - internal class TestBeatmapModelManager : BeatmapModelManager { private readonly TestBeatmapManager testBeatmapManager; @@ -205,6 +191,17 @@ namespace osu.Game.Tests.Online } } + internal class TestBeatmapModelDownloader : BeatmapModelDownloader + { + public TestBeatmapModelDownloader(IModelImporter importer, IAPIProvider apiProvider, GameHost gameHost) + : base(importer, apiProvider) + { + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(IBeatmapSetInfo set, bool minimiseDownloadSize) + => new TestDownloadRequest(set); + } + private class TestDownloadRequest : ArchiveDownloadRequest { public new void SetProgress(float progress) => base.SetProgress(progress); diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index cae7781db7..5effc1f215 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -23,6 +23,11 @@ namespace osu.Game.Tests.Visual.Beatmaps { public class TestSceneBeatmapCard : OsuTestScene { + /// + /// All cards on this scene use a common online ID to ensure that map download, preview tracks, etc. can be tested manually with online sources. + /// + private const int online_id = 163112; + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; private APIBeatmapSet[] testCases; @@ -38,7 +43,6 @@ namespace osu.Game.Tests.Visual.Beatmaps var normal = CreateAPIBeatmapSet(Ruleset.Value); normal.HasVideo = true; normal.HasStoryboard = true; - normal.OnlineID = 241526; var withStatistics = CreateAPIBeatmapSet(Ruleset.Value); withStatistics.Title = withStatistics.TitleUnicode = "play favourite stats"; @@ -106,6 +110,9 @@ namespace osu.Game.Tests.Visual.Beatmaps explicitFeaturedMap, longName }; + + foreach (var testCase in testCases) + testCase.OnlineID = online_id; } private APIBeatmapSet getUndownloadableBeatmapSet() => new APIBeatmapSet @@ -191,9 +198,9 @@ namespace osu.Game.Tests.Visual.Beatmaps private void ensureSoleilyRemoved() { AddUntilStep("ensure manager loaded", () => beatmaps != null); - AddStep("remove soleily", () => + AddStep("remove map", () => { - var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == 241526); + var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == online_id); if (beatmap != null) beatmaps.Delete(beatmap); }); diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs new file mode 100644 index 0000000000..a5b52f75f6 --- /dev/null +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Beatmaps.Drawables.Cards.Buttons; +using osu.Game.Overlays; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Beatmaps +{ + public class TestSceneBeatmapCardThumbnail : OsuManualInputManagerTestScene + { + private PlayButton playButton => this.ChildrenOfType().Single(); + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + [Test] + public void TestThumbnailPreview() + { + BeatmapCardThumbnail thumbnail = null; + + AddStep("create thumbnail", () => + { + var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value); + beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online. + + Child = thumbnail = new BeatmapCardThumbnail(beatmapSet) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200) + }; + }); + AddStep("enable dim", () => thumbnail.Dimmed.Value = true); + AddUntilStep("button visible", () => playButton.IsPresent); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(playButton); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for start", () => playButton.Playing.Value && playButton.Enabled.Value); + iconIs(FontAwesome.Solid.Stop); + + AddStep("click again", () => + { + InputManager.MoveMouseTo(playButton); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for stop", () => !playButton.Playing.Value && playButton.Enabled.Value); + iconIs(FontAwesome.Solid.Play); + + AddStep("click again", () => + { + InputManager.MoveMouseTo(playButton); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for start", () => playButton.Playing.Value && playButton.Enabled.Value); + iconIs(FontAwesome.Solid.Stop); + + AddStep("disable dim", () => thumbnail.Dimmed.Value = false); + AddWaitStep("wait some", 3); + AddAssert("button still visible", () => playButton.IsPresent); + + // The track plays in real-time, so we need to check for progress in increments to avoid timeout. + AddUntilStep("progress > 0.25", () => thumbnail.ChildrenOfType().Single().Progress.Value > 0.25); + AddUntilStep("progress > 0.5", () => thumbnail.ChildrenOfType().Single().Progress.Value > 0.5); + AddUntilStep("progress > 0.75", () => thumbnail.ChildrenOfType().Single().Progress.Value > 0.75); + + AddUntilStep("wait for track to end", () => !playButton.Playing.Value); + AddUntilStep("button hidden", () => !playButton.IsPresent); + } + + private void iconIs(IconUsage usage) => AddUntilStep("icon is correct", () => playButton.ChildrenOfType().Any(icon => icon.Icon.Equals(usage))); + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index e1e869cfbf..f89be0adf3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -69,7 +69,10 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - PushAndConfirm(() => new PlaySongSelect()); + Screens.Select.SongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new PlaySongSelect()); + AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded); AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault); AddStep("Open options", () => InputManager.Key(Key.F3)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs index dcb01b83cc..ccfae1deef 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs @@ -78,6 +78,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("playlist item is not expired", () => Client.APIRoom?.Playlist[1].Expired == false); } + [Test] + public void TestCorrectItemSelectedAfterNewItemAdded() + { + addItem(() => OtherBeatmap); + AddAssert("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID); + } + private void addItem(Func beatmap) { AddStep("click edit button", () => @@ -86,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.IsLoaded); + AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap())); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 1d0401832f..13d98145a1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -13,6 +13,7 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; @@ -22,6 +23,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Beatmaps; +using osu.Game.Users.Drawables; using osuTK; using osuTK.Input; @@ -34,6 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager manager; private RulesetStore rulesets; + [Cached(typeof(UserLookupCache))] + private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache(); + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -304,6 +309,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); } + [TestCase(false)] + [TestCase(true)] + public void TestWithOwner(bool withOwner) + { + createPlaylist(false, false, withOwner); + + AddAssert("owner visible", () => playlist.ChildrenOfType().All(a => a.IsPresent == withOwner)); + } + private void moveToItem(int index, Vector2? offset = null) => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); @@ -327,11 +341,11 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); - private void createPlaylist(bool allowEdit, bool allowSelection) + private void createPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) { AddStep("create playlist", () => { - Child = playlist = new TestPlaylist(allowEdit, allowSelection) + Child = playlist = new TestPlaylist(allowEdit, allowSelection, showItemOwner) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -343,6 +357,7 @@ namespace osu.Game.Tests.Visual.Multiplayer playlist.Items.Add(new PlaylistItem { ID = i, + OwnerID = 2, Beatmap = { Value = i % 2 == 1 @@ -390,6 +405,7 @@ namespace osu.Game.Tests.Visual.Multiplayer playlist.Items.Add(new PlaylistItem { ID = index++, + OwnerID = 2, Beatmap = { Value = b }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, RequiredMods = @@ -409,8 +425,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { public new IReadOnlyDictionary> ItemMap => base.ItemMap; - public TestPlaylist(bool allowEdit, bool allowSelection) - : base(allowEdit, allowSelection) + public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false) + : base(allowEdit, allowSelection, showItemOwner: showItemOwner) { } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index efa5c481ed..1de7289446 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.IsLoaded); + AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded); BeatmapInfo otherBeatmap = null; AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index c4833b5226..4521a7fa0f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -204,7 +204,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // edit playlist item AddStep("Press select", () => InputManager.Key(Key.Enter)); - AddUntilStep("wait for song select", () => InputManager.ChildrenOfType().FirstOrDefault() != null); + AddUntilStep("wait for song select", () => InputManager.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); // select beatmap AddStep("Press select", () => InputManager.Key(Key.Enter)); @@ -253,7 +253,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } - }); + }, API.LocalUser.Value); }); AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); @@ -283,7 +283,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } - }); + }, API.LocalUser.Value); }); AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); @@ -336,7 +336,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } - }); + }, API.LocalUser.Value); }); AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); @@ -597,7 +597,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Ruleset = { Value = new OsuRuleset().RulesetInfo }, } } - }); + }, API.LocalUser.Value); }); AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 6b67a979e5..84b24ba3a1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value))); - AddUntilStep("wait for present", () => songSelect.IsCurrentScreen()); + AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 6d761105c1..35c66e8cda 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(SelectedRoom.Value))); - AddUntilStep("wait for present", () => songSelect.IsCurrentScreen()); + AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); } [Test] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 497c68cf2e..664c186cf8 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -66,7 +66,9 @@ namespace osu.Game.Tests.Visual.Navigation { Player player = null; - PushAndConfirm(() => new TestPlaySongSelect()); + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); @@ -98,7 +100,9 @@ namespace osu.Game.Tests.Visual.Navigation IWorkingBeatmap beatmap() => Game.Beatmap.Value; - PushAndConfirm(() => new TestPlaySongSelect()); + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); @@ -130,7 +134,9 @@ namespace osu.Game.Tests.Visual.Navigation IWorkingBeatmap beatmap() => Game.Beatmap.Value; - PushAndConfirm(() => new TestPlaySongSelect()); + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 5906979bc4..f637c715a1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -572,7 +572,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add mixed ruleset beatmapset", () => { - testMixed = TestResources.CreateTestBeatmapSetInfo(); + testMixed = TestResources.CreateTestBeatmapSetInfo(3); for (int i = 0; i <= 2; i++) { @@ -595,7 +595,7 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapSetInfo testSingle = null; AddStep("add single ruleset beatmapset", () => { - testSingle = TestResources.CreateTestBeatmapSetInfo(); + testSingle = TestResources.CreateTestBeatmapSetInfo(3); testSingle.Beatmaps.ForEach(b => { b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); @@ -615,7 +615,7 @@ namespace osu.Game.Tests.Visual.SongSelect List manySets = new List(); for (int i = 1; i <= 50; i++) - manySets.Add(TestResources.CreateTestBeatmapSetInfo(i)); + manySets.Add(TestResources.CreateTestBeatmapSetInfo(3)); loadBeatmaps(manySets); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 2a2df777f7..aa36bde030 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); return dependencies; } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 7353e47229..9f0f4a6b8b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); beatmapInfo = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Value.Beatmaps[0]; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 0594cd1316..d920b194b5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -29,12 +29,11 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, IModelImporter, IWorkingBeatmapCache, IDisposable + public class BeatmapManager : IModelManager, IModelFileManager, IModelImporter, IWorkingBeatmapCache, IDisposable { public ITrackStore BeatmapTrackStore { get; } private readonly BeatmapModelManager beatmapModelManager; - private readonly BeatmapModelDownloader beatmapModelDownloader; private readonly WorkingBeatmapCache workingBeatmapCache; private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue; @@ -46,7 +45,6 @@ namespace osu.Game.Beatmaps BeatmapTrackStore = audioManager.GetTrackStore(userResources); beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host); - beatmapModelDownloader = CreateBeatmapModelDownloader(beatmapModelManager, api, host); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); workingBeatmapCache.BeatmapManager = beatmapModelManager; @@ -59,11 +57,6 @@ namespace osu.Game.Beatmaps } } - protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(IModelImporter modelManager, IAPIProvider api, GameHost host) - { - return new BeatmapModelDownloader(modelManager, api, host); - } - protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) { return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host); @@ -185,11 +178,7 @@ namespace osu.Game.Beatmaps /// public Action PostNotification { - set - { - beatmapModelManager.PostNotification = value; - beatmapModelDownloader.PostNotification = value; - } + set => beatmapModelManager.PostNotification = value; } /// @@ -225,21 +214,6 @@ namespace osu.Game.Beatmaps remove => beatmapModelManager.ItemRemoved -= value; } - public Task ImportFromStableAsync(StableStorage stableStorage) - { - return beatmapModelManager.ImportFromStableAsync(stableStorage); - } - - public void Export(BeatmapSetInfo item) - { - beatmapModelManager.Export(item); - } - - public void ExportModelTo(BeatmapSetInfo model, Stream outputStream) - { - beatmapModelManager.ExportModelTo(model, outputStream); - } - public void Update(BeatmapSetInfo item) { beatmapModelManager.Update(item); @@ -267,28 +241,6 @@ namespace osu.Game.Beatmaps #endregion - #region Implementation of IModelDownloader - - public event Action> DownloadBegan - { - add => beatmapModelDownloader.DownloadBegan += value; - remove => beatmapModelDownloader.DownloadBegan -= value; - } - - public event Action> DownloadFailed - { - add => beatmapModelDownloader.DownloadFailed += value; - remove => beatmapModelDownloader.DownloadFailed -= value; - } - - public bool Download(IBeatmapSetInfo model, bool minimiseDownloadSize = false) => - beatmapModelDownloader.Download(model, minimiseDownloadSize); - - public ArchiveDownloadRequest GetExistingDownload(IBeatmapSetInfo model) => - beatmapModelDownloader.GetExistingDownload(model); - - #endregion - #region Implementation of ICanAcceptFiles public Task Import(params string[] paths) diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs index a170edc9f8..d31730ca15 100644 --- a/osu.Game/Beatmaps/BeatmapModelDownloader.cs +++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -16,8 +15,8 @@ namespace osu.Game.Beatmaps public override ArchiveDownloadRequest GetExistingDownload(IBeatmapSetInfo model) => CurrentDownloads.Find(r => r.Model.OnlineID == model.OnlineID); - public BeatmapModelDownloader(IModelImporter beatmapImporter, IAPIProvider api, GameHost host = null) - : base(beatmapImporter, api, host) + public BeatmapModelDownloader(IModelImporter beatmapImporter, IAPIProvider api) + : base(beatmapImporter, api) { } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index ae395c6da6..d0c41e0fb8 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps /// Handles ef-core storage of beatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapModelManager : ArchiveModelManager, IBeatmapModelManager + public class BeatmapModelManager : ArchiveModelManager { /// /// Fired when a single difficulty has been hidden. @@ -58,10 +58,6 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - protected override string ImportFromStablePath => "."; - - protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); - private readonly BeatmapStore beatmaps; private readonly RulesetStore rulesets; @@ -216,7 +212,7 @@ namespace osu.Game.Beatmaps var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); // metadata may have changed; update the path with the standard format. - beatmapInfo.Path = GetValidFilename($"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu"); + beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename(); beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 5bb9eefc6e..ac7067edda 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -36,8 +36,7 @@ namespace osu.Game.Beatmaps public BeatmapOnlineStatus Status { get; set; } = BeatmapOnlineStatus.None; - [NotNull] - public List Files { get; set; } = new List(); + public List Files { get; } = new List(); /// /// The maximum star difficulty of all beatmaps in this set. @@ -96,7 +95,7 @@ namespace osu.Game.Beatmaps IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata ?? Beatmaps.FirstOrDefault()?.Metadata ?? new BeatmapMetadata(); IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; - IEnumerable IBeatmapSetInfo.Files => Files; + IEnumerable IHasNamedFiles.Files => Files; #endregion } diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index e3af253db9..76b1166626 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -23,7 +25,6 @@ using osu.Game.Overlays.BeatmapSet; using osuTK; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Resources.Localisation.Web; -using osuTK.Graphics; using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton; namespace osu.Game.Beatmaps.Drawables.Cards @@ -42,27 +43,23 @@ namespace osu.Game.Beatmaps.Drawables.Cards private readonly BeatmapDownloadTracker downloadTracker; - private UpdateableOnlineBeatmapSetCover leftCover; - private FillFlowContainer leftIconArea; + private BeatmapCardThumbnail thumbnail = null!; - private Container rightAreaBackground; - private Container rightAreaButtons; + private Container rightAreaBackground = null!; + private Container rightAreaButtons = null!; - private Container mainContent; - private BeatmapCardContentBackground mainContentBackground; + private Container mainContent = null!; + private BeatmapCardContentBackground mainContentBackground = null!; + private FillFlowContainer statisticsContainer = null!; - private GridContainer titleContainer; - private GridContainer artistContainer; - private FillFlowContainer statisticsContainer; - - private FillFlowContainer idleBottomContent; - private BeatmapCardDownloadProgressBar downloadProgressBar; + private FillFlowContainer idleBottomContent = null!; + private BeatmapCardDownloadProgressBar downloadProgressBar = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; public BeatmapCard(APIBeatmapSet beatmapSet) : base(HoverSampleSet.Submit) @@ -72,14 +69,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards downloadTracker = new BeatmapDownloadTracker(beatmapSet); } - [BackgroundDependencyLoader] - private void load() + [BackgroundDependencyLoader(true)] + private void load(BeatmapSetOverlay? beatmapSetOverlay) { Width = width; Height = height; CornerRadius = corner_radius; Masking = true; + FillFlowContainer leftIconArea; + GridContainer titleContainer; + GridContainer artistContainer; + InternalChildren = new Drawable[] { downloadTracker, @@ -98,24 +99,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards Colour = Colour4.White }, }, - new Container + thumbnail = new BeatmapCardThumbnail(beatmapSet) { Name = @"Left (icon) area", Size = new Vector2(height), - Children = new Drawable[] + Padding = new MarginPadding { Right = corner_radius }, + Child = leftIconArea = new FillFlowContainer { - leftCover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) - { - RelativeSizeAxes = Axes.Both, - OnlineInfo = beatmapSet - }, - leftIconArea = new FillFlowContainer - { - Margin = new MarginPadding(5), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(1) - } + Margin = new MarginPadding(5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) } }, new Container @@ -319,10 +313,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards }; if (beatmapSet.HasVideo) - leftIconArea.Add(new IconPill(FontAwesome.Solid.Film)); + leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); if (beatmapSet.HasStoryboard) - leftIconArea.Add(new IconPill(FontAwesome.Solid.Image)); + leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) }); if (beatmapSet.HasExplicitContent) { @@ -343,6 +337,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards Margin = new MarginPadding { Left = 5 } }; } + + Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSet.OnlineID); } protected override void LoadComplete() @@ -395,10 +391,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards if (IsHovered) targetWidth = targetWidth - icon_area_width + corner_radius; + thumbnail.Dimmed.Value = IsHovered; + mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint); mainContentBackground.Dimmed.Value = IsHovered; - leftCover.FadeColour(IsHovered ? OsuColour.Gray(0.2f) : Color4.White, TRANSITION_DURATION, Easing.OutQuint); statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, TRANSITION_DURATION, Easing.OutQuint); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs new file mode 100644 index 0000000000..f11a5916e1 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps.Drawables.Cards.Buttons; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardThumbnail : Container + { + public BindableBool Dimmed { get; } = new BindableBool(); + + public new MarginPadding Padding + { + get => foreground.Padding; + set => foreground.Padding = value; + } + + private readonly UpdateableOnlineBeatmapSetCover cover; + private readonly Container foreground; + private readonly PlayButton playButton; + private readonly SmoothCircularProgress progress; + private readonly Container content; + + protected override Container Content => content; + + public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) + { + InternalChildren = new Drawable[] + { + cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + { + RelativeSizeAxes = Axes.Both, + OnlineInfo = beatmapSetInfo + }, + foreground = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + playButton = new PlayButton(beatmapSetInfo) + { + RelativeSizeAxes = Axes.Both + }, + progress = new SmoothCircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(50), + InnerRadius = 0.2f + }, + content = new Container + { + RelativeSizeAxes = Axes.Both + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + progress.Colour = colourProvider.Highlight1; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Dimmed.BindValueChanged(_ => updateState()); + + playButton.Playing.BindValueChanged(_ => updateState(), true); + ((IBindable)progress.Current).BindTo(playButton.Progress); + + FinishTransforms(true); + } + + private void updateState() + { + bool shouldDim = Dimmed.Value || playButton.Playing.Value; + + playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + cover.FadeColour(shouldDim ? OsuColour.Gray(0.2f) : Color4.White, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs index ad9caf7e34..e362e3abeb 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs @@ -54,6 +54,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons protected readonly SpriteIcon Icon; + protected override Container Content => content; + private readonly Container content; protected BeatmapCardIconButton() @@ -61,7 +63,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons Origin = Anchor.Centre; Anchor = Anchor.Centre; - Child = content = new Container + base.Content.Add(content = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -75,7 +77,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons Anchor = Anchor.Centre } } - }; + }); Size = new Vector2(24); IconSize = 12; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs index 7430fce1c8..c94e335e8f 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs @@ -3,14 +3,17 @@ #nullable enable +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Resources.Localisation.Web; +using osuTK; namespace osu.Game.Beatmaps.Drawables.Cards.Buttons { @@ -23,13 +26,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private Bindable preferNoVideo = null!; + private readonly LoadingSpinner spinner; + [Resolved] - private BeatmapManager beatmaps { get; set; } = null!; + private BeatmapModelDownloader beatmaps { get; set; } = null!; public DownloadButton(APIBeatmapSet beatmapSet) { Icon.Icon = FontAwesome.Solid.Download; + Content.Add(spinner = new LoadingSpinner { Size = new Vector2(IconSize) }); + this.beatmapSet = beatmapSet; } @@ -49,21 +56,44 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private void updateState() { - this.FadeTo(state.Value != DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - - if (beatmapSet.Availability.DownloadDisabled) + switch (state.Value) { - Enabled.Value = false; - TooltipText = BeatmapsetsStrings.AvailabilityDisabled; - return; + case DownloadState.Downloading: + case DownloadState.Importing: + Action = null; + TooltipText = string.Empty; + spinner.Show(); + Icon.Hide(); + break; + + case DownloadState.LocallyAvailable: + Action = null; + TooltipText = string.Empty; + this.FadeOut(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + break; + + case DownloadState.NotDownloaded: + if (beatmapSet.Availability.DownloadDisabled) + { + Enabled.Value = false; + TooltipText = BeatmapsetsStrings.AvailabilityDisabled; + return; + } + + Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); + this.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + spinner.Hide(); + Icon.Show(); + + if (!beatmapSet.HasVideo) + TooltipText = BeatmapsetsStrings.PanelDownloadAll; + else + TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; + break; + + default: + throw new InvalidOperationException($"Unknown {nameof(DownloadState)} specified."); } - - if (!beatmapSet.HasVideo) - TooltipText = BeatmapsetsStrings.PanelDownloadAll; - else - TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo; - - Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs new file mode 100644 index 0000000000..4574d37da0 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Audio; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards.Buttons +{ + public class PlayButton : OsuHoverContainer + { + public IBindable Progress => progress; + private readonly BindableDouble progress = new BindableDouble(); + + public BindableBool Playing { get; } = new BindableBool(); + + private readonly IBeatmapSetInfo beatmapSetInfo; + + protected override IEnumerable EffectTargets => icon.Yield(); + + private readonly SpriteIcon icon; + private readonly LoadingSpinner loadingSpinner; + + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + + private PreviewTrack? previewTrack; + + public PlayButton(IBeatmapSetInfo beatmapSetInfo) + { + this.beatmapSetInfo = beatmapSetInfo; + + Anchor = Origin = Anchor.Centre; + + Children = new Drawable[] + { + icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Play, + Size = new Vector2(14) + }, + loadingSpinner = new LoadingSpinner + { + Size = new Vector2(14) + } + }; + + Action = () => Playing.Toggle(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + HoverColour = colours.Yellow; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playing.BindValueChanged(updateState, true); + } + + protected override void Update() + { + base.Update(); + + if (Playing.Value && previewTrack != null && previewTrack.TrackLoaded) + progress.Value = previewTrack.CurrentTime / previewTrack.Length; + else + progress.Value = 0; + } + + private void updateState(ValueChangedEvent playing) + { + icon.Icon = playing.NewValue ? FontAwesome.Solid.Stop : FontAwesome.Solid.Play; + + if (!playing.NewValue) + { + stopPreview(); + return; + } + + if (previewTrack == null) + { + toggleLoading(true); + LoadComponentAsync(previewTrack = previewTrackManager.Get(beatmapSetInfo), onPreviewLoaded); + } + else + tryStartPreview(); + } + + private void stopPreview() + { + toggleLoading(false); + Playing.Value = false; + previewTrack?.Stop(); + } + + private void onPreviewLoaded(PreviewTrack loadedPreview) + { + // another async load might have completed before this one. + // if so, do not make any changes. + if (loadedPreview != previewTrack) + return; + + AddInternal(loadedPreview); + toggleLoading(false); + + loadedPreview.Stopped += () => Schedule(() => Playing.Value = false); + + if (Playing.Value) + tryStartPreview(); + } + + private void tryStartPreview() + { + if (previewTrack?.Start() == false) + Playing.Value = false; + } + + private void toggleLoading(bool loading) + { + Enabled.Value = !loading; + icon.FadeTo(loading ? 0 : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + loadingSpinner.State.Value = loading ? Visibility.Visible : Visibility.Hidden; + } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapModelManager.cs b/osu.Game/Beatmaps/IBeatmapModelManager.cs deleted file mode 100644 index 8c243c2b77..0000000000 --- a/osu.Game/Beatmaps/IBeatmapModelManager.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Database; - -namespace osu.Game.Beatmaps -{ - public interface IBeatmapModelManager : IModelManager - { - /// - /// Provide an online lookup queue component to handle populating online beatmap metadata. - /// - BeatmapOnlineLookupQueue OnlineLookupQueue { set; } - - /// - /// Provide a working beatmap cache, used to invalidate entries on changes. - /// - IWorkingBeatmapCache WorkingBeatmapCache { set; } - } -} diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs index aa114c8472..9755120457 100644 --- a/osu.Game/Beatmaps/IBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.cs @@ -12,7 +12,7 @@ namespace osu.Game.Beatmaps /// /// A representation of a collection of beatmap difficulties, generally packaged as an ".osz" archive. /// - public interface IBeatmapSetInfo : IHasOnlineID, IEquatable + public interface IBeatmapSetInfo : IHasOnlineID, IEquatable, IHasNamedFiles { /// /// The date when this beatmap was imported. @@ -29,11 +29,6 @@ namespace osu.Game.Beatmaps /// IEnumerable Beatmaps { get; } - /// - /// All files used by this set. - /// - IEnumerable Files { get; } - /// /// The maximum star difficulty of all beatmaps in this set. /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index a0e8105285..e73f4a7f6e 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -20,7 +20,6 @@ using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.IPC; using osu.Game.Overlays.Notifications; -using SharpCompress.Archives.Zip; namespace osu.Game.Database { @@ -82,8 +81,6 @@ namespace osu.Game.Database // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; - private readonly Storage exportStorage; - protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes modelStore, IIpcHost importHost = null) { ContextFactory = contextFactory; @@ -92,8 +89,6 @@ namespace osu.Game.Database ModelStore.ItemUpdated += item => handleEvent(() => ItemUpdated?.Invoke(item)); ModelStore.ItemRemoved += item => handleEvent(() => ItemRemoved?.Invoke(item)); - exportStorage = storage.GetStorageForDirectory(@"exports"); - Files = new FileStore(contextFactory, storage); if (importHost != null) @@ -392,7 +387,8 @@ namespace osu.Game.Database { LogForModel(item, @"Beginning import..."); - item.Files = archive != null ? createFileInfos(archive, Files) : new List(); + if (archive != null) + item.Files.AddRange(createFileInfos(archive, Files)); item.Hash = ComputeHash(item); await Populate(item, archive, cancellationToken).ConfigureAwait(false); @@ -451,41 +447,6 @@ namespace osu.Game.Database return item.ToEntityFrameworkLive(); }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false); - /// - /// Exports an item to a legacy (.zip based) package. - /// - /// The item to export. - public void Export(TModel item) - { - var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID); - - if (retrievedItem == null) - throw new ArgumentException(@"Specified model could not be found", nameof(item)); - - string filename = $"{GetValidFilename(item.ToString())}{HandledExtensions.First()}"; - - using (var stream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create)) - ExportModelTo(retrievedItem, stream); - - exportStorage.PresentFileExternally(filename); - } - - /// - /// Exports an item to the given output stream. - /// - /// The item to export. - /// The output stream to export to. - public virtual void ExportModelTo(TModel model, Stream outputStream) - { - using (var archive = ZipArchive.Create()) - { - foreach (var file in model.Files) - archive.AddEntry(file.Filename, Files.Storage.GetStream(file.FileInfo.GetStoragePath())); - - archive.SaveTo(outputStream); - } - } - /// /// Replace an existing file with a new version. /// @@ -727,17 +688,6 @@ namespace osu.Game.Database #region osu-stable import - /// - /// The relative path from osu-stable's data directory to import items from. - /// - protected virtual string ImportFromStablePath => null; - - /// - /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in . - /// - protected virtual IEnumerable GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath) - .Select(path => storage.GetFullPath(path)); - /// /// Whether this specified path should be removed after successful import. /// @@ -745,29 +695,6 @@ namespace osu.Game.Database /// Whether to perform deletion. protected virtual bool ShouldDeleteArchive(string path) => false; - public Task ImportFromStableAsync(StableStorage stableStorage) - { - var storage = PrepareStableStorage(stableStorage); - - // Handle situations like when the user does not have a Skins folder. - if (!storage.ExistsDirectory(ImportFromStablePath)) - { - string fullPath = storage.GetFullPath(ImportFromStablePath); - - Logger.Log(@$"Folder ""{fullPath}"" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error); - return Task.CompletedTask; - } - - return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false)); - } - - /// - /// Run any required traversal operations on the stable storage location before performing operations. - /// - /// The stable storage. - /// The usable storage. Return the unchanged if no traversal is required. - protected virtual Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage; - #endregion /// @@ -908,18 +835,5 @@ namespace osu.Game.Database // this doesn't follow the SHA2 hashing schema intentionally, so such entries on the data store can be identified. return Guid.NewGuid().ToString(); } - - private readonly char[] invalidFilenameCharacters = Path.GetInvalidFileNameChars() - // Backslash is added to avoid issues when exporting to zip. - // See SharpCompress filename normalisation https://github.com/adamhathcock/sharpcompress/blob/a1e7c0068db814c9aa78d86a94ccd1c761af74bd/src/SharpCompress/Writers/Zip/ZipWriter.cs#L143. - .Append('\\') - .ToArray(); - - protected string GetValidFilename(string filename) - { - foreach (char c in invalidFilenameCharacters) - filename = filename.Replace(c, '_'); - return filename; - } } } diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index a11efba54b..03e1c014b2 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -19,7 +19,7 @@ namespace osu.Game.Database /// The object to use as a reference when negotiating a local instance. /// An optional lookup source which will be used to query and populate a freshly retrieved replacement. If not provided, the refreshed object will still be returned but will not have any includes. /// A valid EF-stored type. - protected virtual void Refresh(ref T obj, IQueryable lookupSource = null) where T : class, IHasPrimaryKey + protected void Refresh(ref T obj, IQueryable lookupSource = null) where T : class, IHasPrimaryKey { using (var usage = ContextFactory.GetForWrite()) { diff --git a/osu.Game/Database/EntityFrameworkLive.cs b/osu.Game/Database/EntityFrameworkLive.cs index 1d7b53911a..25c0778746 100644 --- a/osu.Game/Database/EntityFrameworkLive.cs +++ b/osu.Game/Database/EntityFrameworkLive.cs @@ -3,12 +3,15 @@ using System; +#nullable enable + namespace osu.Game.Database { public class EntityFrameworkLive : ILive where T : class { public EntityFrameworkLive(T item) { + IsManaged = true; // no way to really know. Value = item; } @@ -29,6 +32,10 @@ namespace osu.Game.Database perform(Value); } + public bool IsManaged { get; } + public T Value { get; } + + public bool Equals(ILive? other) => ID == other?.ID; } } diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs index f6aa941ec2..3f6531832f 100644 --- a/osu.Game/Database/IHasFiles.cs +++ b/osu.Game/Database/IHasFiles.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; namespace osu.Game.Database { @@ -12,7 +13,8 @@ namespace osu.Game.Database public interface IHasFiles where TFile : INamedFileInfo { - List Files { get; set; } + [NotNull] + List Files { get; } string Hash { get; set; } } diff --git a/osu.Game/Database/IHasNamedFiles.cs b/osu.Game/Database/IHasNamedFiles.cs new file mode 100644 index 0000000000..08906aaa08 --- /dev/null +++ b/osu.Game/Database/IHasNamedFiles.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; + +namespace osu.Game.Database +{ + public interface IHasNamedFiles + { + /// + /// All files used by this model. + /// + IEnumerable Files { get; } + } +} diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs index 9359b09eaf..a863339f11 100644 --- a/osu.Game/Database/ILive.cs +++ b/osu.Game/Database/ILive.cs @@ -9,7 +9,8 @@ namespace osu.Game.Database /// A wrapper to provide access to database backed classes in a thread-safe manner. /// /// The databased type. - public interface ILive where T : class // TODO: Add IHasGuidPrimaryKey once we don't need EF support any more. + public interface ILive : IEquatable> + where T : class // TODO: Add IHasGuidPrimaryKey once we don't need EF support any more. { Guid ID { get; } @@ -31,6 +32,11 @@ namespace osu.Game.Database /// The action to perform. void PerformWrite(Action perform); + /// + /// Whether this instance is tracking data which is managed by the database backing. + /// + bool IsManaged { get; } + /// /// Resolve the value of this instance on the current thread's context. /// diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 15ad455f21..779d0522f7 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -3,9 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using osu.Game.IO; namespace osu.Game.Database { @@ -26,24 +23,6 @@ namespace osu.Game.Database /// event Action ItemRemoved; - /// - /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. - /// - Task ImportFromStableAsync(StableStorage stableStorage); - - /// - /// Exports an item to a legacy (.zip based) package. - /// - /// The item to export. - void Export(TModel item); - - /// - /// Exports an item to the given output stream. - /// - /// The item to export. - /// The output stream to export to. - void ExportModelTo(TModel model, Stream outputStream); - /// /// Perform an update of the specified item. /// TODO: Support file additions/removals. diff --git a/osu.Game/Database/IPostImports.cs b/osu.Game/Database/IPostImports.cs index b3b83f23ef..adb3a7108d 100644 --- a/osu.Game/Database/IPostImports.cs +++ b/osu.Game/Database/IPostImports.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; namespace osu.Game.Database { - public interface IPostImports + public interface IPostImports where TModel : class { /// diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs new file mode 100644 index 0000000000..fb8ee8f5f5 --- /dev/null +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform; +using osu.Game.Beatmaps; + +namespace osu.Game.Database +{ + public class LegacyBeatmapExporter : LegacyExporter + { + protected override string FileExtension => ".osz"; + + public LegacyBeatmapExporter(Storage storage) + : base(storage) + { + } + } +} diff --git a/osu.Game/Database/LegacyBeatmapImporter.cs b/osu.Game/Database/LegacyBeatmapImporter.cs new file mode 100644 index 0000000000..97f6eba6c2 --- /dev/null +++ b/osu.Game/Database/LegacyBeatmapImporter.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.IO; + +namespace osu.Game.Database +{ + public class LegacyBeatmapImporter : LegacyModelImporter + { + protected override string ImportFromStablePath => "."; + + protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); + + public LegacyBeatmapImporter(IModelImporter importer) + : base(importer) + { + } + } +} diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs new file mode 100644 index 0000000000..802ccec6ed --- /dev/null +++ b/osu.Game/Database/LegacyExporter.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using osu.Framework.Platform; +using osu.Game.Extensions; +using SharpCompress.Archives.Zip; + +namespace osu.Game.Database +{ + /// + /// A class which handles exporting legacy user data of a single type from osu-stable. + /// + public abstract class LegacyExporter + where TModel : class, IHasNamedFiles + { + /// + /// The file extension for exports (including the leading '.'). + /// + protected abstract string FileExtension { get; } + + protected readonly Storage UserFileStorage; + + private readonly Storage exportStorage; + + protected LegacyExporter(Storage storage) + { + exportStorage = storage.GetStorageForDirectory(@"exports"); + UserFileStorage = storage.GetStorageForDirectory(@"files"); + } + + /// + /// Exports an item to a legacy (.zip based) package. + /// + /// The item to export. + public void Export(TModel item) + { + string filename = $"{item.ToString().GetValidArchiveContentFilename()}{FileExtension}"; + + using (var stream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create)) + ExportModelTo(item, stream); + + exportStorage.PresentFileExternally(filename); + } + + /// + /// Exports an item to the given output stream. + /// + /// The item to export. + /// The output stream to export to. + public virtual void ExportModelTo(TModel model, Stream outputStream) + { + using (var archive = ZipArchive.Create()) + { + foreach (var file in model.Files) + archive.AddEntry(file.Filename, UserFileStorage.GetStream(file.File.GetStoragePath())); + + archive.SaveTo(outputStream); + } + } + } +} diff --git a/osu.Game/Database/StableImportManager.cs b/osu.Game/Database/LegacyImportManager.cs similarity index 85% rename from osu.Game/Database/StableImportManager.cs rename to osu.Game/Database/LegacyImportManager.cs index fe8c14c085..4dc26b18bb 100644 --- a/osu.Game/Database/StableImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -19,7 +19,10 @@ using osu.Game.Skinning; namespace osu.Game.Database { - public class StableImportManager : Component + /// + /// Handles migration of legacy user data from osu-stable. + /// + public class LegacyImportManager : Component { [Resolved] private SkinManager skins { get; set; } @@ -53,16 +56,16 @@ namespace osu.Game.Database Task beatmapImportTask = Task.CompletedTask; if (content.HasFlagFast(StableContent.Beatmaps)) - importTasks.Add(beatmapImportTask = beatmaps.ImportFromStableAsync(stableStorage)); + importTasks.Add(beatmapImportTask = new LegacyBeatmapImporter(beatmaps).ImportFromStableAsync(stableStorage)); if (content.HasFlagFast(StableContent.Skins)) - importTasks.Add(skins.ImportFromStableAsync(stableStorage)); + importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); if (content.HasFlagFast(StableContent.Collections)) importTasks.Add(beatmapImportTask.ContinueWith(_ => collections.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); if (content.HasFlagFast(StableContent.Scores)) - importTasks.Add(beatmapImportTask.ContinueWith(_ => scores.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); + importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); } diff --git a/osu.Game/Database/LegacyModelImporter.cs b/osu.Game/Database/LegacyModelImporter.cs new file mode 100644 index 0000000000..dacb7327ea --- /dev/null +++ b/osu.Game/Database/LegacyModelImporter.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.IO; + +namespace osu.Game.Database +{ + /// + /// A class which handles importing legacy user data of a single type from osu-stable. + /// + public abstract class LegacyModelImporter + where TModel : class + { + /// + /// The relative path from osu-stable's data directory to import items from. + /// + protected virtual string ImportFromStablePath => null; + + /// + /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in . + /// + protected virtual IEnumerable GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath) + .Select(path => storage.GetFullPath(path)); + + protected readonly IModelImporter Importer; + + protected LegacyModelImporter(IModelImporter importer) + { + Importer = importer; + } + + public Task ImportFromStableAsync(StableStorage stableStorage) + { + var storage = PrepareStableStorage(stableStorage); + + // Handle situations like when the user does not have a Skins folder. + if (!storage.ExistsDirectory(ImportFromStablePath)) + { + string fullPath = storage.GetFullPath(ImportFromStablePath); + + Logger.Log(@$"Folder ""{fullPath}"" not available in the target osu!stable installation to import {Importer.HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error); + return Task.CompletedTask; + } + + return Task.Run(async () => await Importer.Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false)); + } + + /// + /// Run any required traversal operations on the stable storage location before performing operations. + /// + /// The stable storage. + /// The usable storage. Return the unchanged if no traversal is required. + protected virtual Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage; + } +} diff --git a/osu.Game/Database/LegacyScoreExporter.cs b/osu.Game/Database/LegacyScoreExporter.cs new file mode 100644 index 0000000000..41f8516880 --- /dev/null +++ b/osu.Game/Database/LegacyScoreExporter.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using osu.Framework.Platform; +using osu.Game.Extensions; +using osu.Game.Scoring; + +namespace osu.Game.Database +{ + public class LegacyScoreExporter : LegacyExporter + { + protected override string FileExtension => ".osr"; + + public LegacyScoreExporter(Storage storage) + : base(storage) + { + } + + public override void ExportModelTo(ScoreInfo model, Stream outputStream) + { + var file = model.Files.SingleOrDefault(); + if (file == null) + return; + + using (var inputStream = UserFileStorage.GetStream(file.FileInfo.GetStoragePath())) + inputStream.CopyTo(outputStream); + } + } +} diff --git a/osu.Game/Database/LegacyScoreImporter.cs b/osu.Game/Database/LegacyScoreImporter.cs new file mode 100644 index 0000000000..48445b7bdb --- /dev/null +++ b/osu.Game/Database/LegacyScoreImporter.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . 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 osu.Framework.Platform; +using osu.Game.Scoring; + +namespace osu.Game.Database +{ + public class LegacyScoreImporter : LegacyModelImporter + { + protected override string ImportFromStablePath => Path.Combine("Data", "r"); + + protected override IEnumerable GetStableImportPaths(Storage storage) + => storage.GetFiles(ImportFromStablePath).Where(p => Importer.HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => storage.GetFullPath(path)); + + public LegacyScoreImporter(IModelImporter importer) + : base(importer) + { + } + } +} diff --git a/osu.Game/Database/LegacySkinExporter.cs b/osu.Game/Database/LegacySkinExporter.cs new file mode 100644 index 0000000000..9432a1b5fc --- /dev/null +++ b/osu.Game/Database/LegacySkinExporter.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform; +using osu.Game.Skinning; + +namespace osu.Game.Database +{ + public class LegacySkinExporter : LegacyExporter + { + protected override string FileExtension => ".osk"; + + public LegacySkinExporter(Storage storage) + : base(storage) + { + } + } +} diff --git a/osu.Game/Database/LegacySkinImporter.cs b/osu.Game/Database/LegacySkinImporter.cs new file mode 100644 index 0000000000..2f05ccae45 --- /dev/null +++ b/osu.Game/Database/LegacySkinImporter.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Skinning; + +namespace osu.Game.Database +{ + public class LegacySkinImporter : LegacyModelImporter + { + protected override string ImportFromStablePath => "Skins"; + + public LegacySkinImporter(IModelImporter importer) + : base(importer) + { + } + } +} diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs index 43ba62dfe0..362bc68cc1 100644 --- a/osu.Game/Database/ModelDownloader.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Humanizer; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Overlays.Notifications; @@ -29,7 +28,7 @@ namespace osu.Game.Database protected readonly List> CurrentDownloads = new List>(); - protected ModelDownloader(IModelImporter importer, IAPIProvider api, IIpcHost importHost = null) + protected ModelDownloader(IModelImporter importer, IAPIProvider api) { this.importer = importer; this.api = api; diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 738ecaea7c..3c5dfeafe8 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -14,6 +14,7 @@ using osu.Framework.Statistics; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; +using osu.Game.Stores; using Realms; #nullable enable @@ -121,6 +122,10 @@ namespace osu.Game.Database transaction.Commit(); } + + // clean up files after dropping any pending deletions. + // in the future we may want to only do this when the game is idle, rather than on every startup. + new RealmFileStore(this, storage).Cleanup(); } /// diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index abb69644d6..73e6715aaa 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -17,6 +17,8 @@ namespace osu.Game.Database { public Guid ID { get; } + public bool IsManaged { get; } + private readonly SynchronizationContext? fetchedContext; private readonly int fetchedThreadId; @@ -33,8 +35,13 @@ namespace osu.Game.Database { this.data = data; - fetchedContext = SynchronizationContext.Current; - fetchedThreadId = Thread.CurrentThread.ManagedThreadId; + if (data.IsManaged) + { + IsManaged = true; + + fetchedContext = SynchronizationContext.Current; + fetchedThreadId = Thread.CurrentThread.ManagedThreadId; + } ID = data.ID; } @@ -75,13 +82,18 @@ namespace osu.Game.Database /// Perform a write operation on this live object. /// /// The action to perform. - public void PerformWrite(Action perform) => + public void PerformWrite(Action perform) + { + if (!IsManaged) + throw new InvalidOperationException("Can't perform writes on a non-managed underlying value"); + PerformRead(t => { var transaction = t.Realm.BeginWrite(); perform(t); transaction.Commit(); }); + } public T Value { @@ -102,10 +114,12 @@ namespace osu.Game.Database } } - private bool originalDataValid => isCorrectThread && data.IsValid; + private bool originalDataValid => !IsManaged || (isCorrectThread && data.IsValid); // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) private bool isCorrectThread => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId; + + public bool Equals(ILive? other) => ID == other?.ID; } } diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index 9f00d21383..2274da0fd4 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO; @@ -124,5 +125,21 @@ namespace osu.Game.Extensions return instance.OnlineID.Equals(other.OnlineID); } + + private static readonly char[] invalid_filename_characters = Path.GetInvalidFileNameChars() + // Backslash is added to avoid issues when exporting to zip. + // See SharpCompress filename normalisation https://github.com/adamhathcock/sharpcompress/blob/a1e7c0068db814c9aa78d86a94ccd1c761af74bd/src/SharpCompress/Writers/Zip/ZipWriter.cs#L143. + .Append('\\') + .ToArray(); + + /// + /// Get a valid filename for use inside a zip file. Avoids backslashes being incorrectly converted to directories. + /// + public static string GetValidArchiveContentFilename(this string filename) + { + foreach (char c in invalid_filename_characters) + filename = filename.Replace(c, '_'); + return filename; + } } } diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 165cf756aa..ebe1ebfe69 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -2,11 +2,8 @@ // 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.Linq.Expressions; -using Microsoft.EntityFrameworkCore; using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Logging; @@ -31,13 +28,6 @@ namespace osu.Game.IO Store = new StorageBackedResourceStore(Storage); } - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// Results from the provided query. - public IEnumerable QueryFiles(Expression> query) => ContextFactory.Get().Set().AsNoTracking().Where(f => f.ReferenceCount > 0).Where(query); - public FileInfo Add(Stream data, bool reference = true) { using (var usage = ContextFactory.GetForWrite()) diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs index fee59633f1..3566ff5321 100644 --- a/osu.Game/Models/RealmBeatmapSet.cs +++ b/osu.Game/Models/RealmBeatmapSet.cs @@ -76,7 +76,6 @@ namespace osu.Game.Models public bool Equals(IBeatmapSetInfo? other) => other is RealmBeatmapSet b && Equals(b); IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; - - IEnumerable IBeatmapSetInfo.Files => Files; + IEnumerable IHasNamedFiles.Files => Files; } } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 9005fa8eb7..57c45faed3 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -136,7 +136,7 @@ namespace osu.Game.Online.API.Requests.Responses IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => metadata; DateTimeOffset IBeatmapSetInfo.DateAdded => throw new NotImplementedException(); - IEnumerable IBeatmapSetInfo.Files => throw new NotImplementedException(); + IEnumerable IHasNamedFiles.Files => throw new NotImplementedException(); double IBeatmapSetInfo.MaxStarDifficulty => throw new NotImplementedException(); double IBeatmapSetInfo.MaxLength => throw new NotImplementedException(); double IBeatmapSetInfo.MaxBPM => BPM; diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs index 0a2d6ca7b0..467d5a9f23 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScoreInfo.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -147,6 +148,7 @@ namespace osu.Game.Online.API.Requests.Responses } public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID }; + IEnumerable IHasNamedFiles.Files => throw new NotImplementedException(); IBeatmapInfo IScoreInfo.Beatmap => Beatmap; } diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 77a8fca1e4..509d5c1b71 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -15,6 +15,9 @@ namespace osu.Game.Online [Resolved(CanBeNull = true)] protected BeatmapManager? Manager { get; private set; } + [Resolved(CanBeNull = true)] + protected BeatmapModelDownloader? Downloader { get; private set; } + private ArchiveDownloadRequest? attachedRequest; public BeatmapDownloadTracker(IBeatmapSetInfo trackedItem) @@ -25,7 +28,7 @@ namespace osu.Game.Online [BackgroundDependencyLoader(true)] private void load() { - if (Manager == null) + if (Manager == null || Downloader == null) return; // Used to interact with manager classes that don't support interface types. Will eventually be replaced. @@ -34,10 +37,10 @@ namespace osu.Game.Online if (Manager.IsAvailableLocally(beatmapSetInfo)) UpdateState(DownloadState.LocallyAvailable); else - attachDownload(Manager.GetExistingDownload(beatmapSetInfo)); + attachDownload(Downloader.GetExistingDownload(beatmapSetInfo)); - Manager.DownloadBegan += downloadBegan; - Manager.DownloadFailed += downloadFailed; + Downloader.DownloadBegan += downloadBegan; + Downloader.DownloadFailed += downloadFailed; Manager.ItemUpdated += itemUpdated; Manager.ItemRemoved += itemRemoved; } @@ -115,10 +118,14 @@ namespace osu.Game.Online base.Dispose(isDisposing); attachDownload(null); + if (Downloader != null) + { + Downloader.DownloadBegan -= downloadBegan; + Downloader.DownloadFailed -= downloadFailed; + } + if (Manager != null) { - Manager.DownloadBegan -= downloadBegan; - Manager.DownloadFailed -= downloadFailed; Manager.ItemUpdated -= itemUpdated; Manager.ItemRemoved -= itemRemoved; } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 26749a23f9..e01c7c9e49 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -14,6 +14,8 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Platform; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -66,6 +68,9 @@ namespace osu.Game.Online.Leaderboards [Resolved] private ScoreManager scoreManager { get; set; } + [Resolved] + private Storage storage { get; set; } + public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true) { Score = score; @@ -394,8 +399,8 @@ namespace osu.Game.Online.Leaderboards if (Score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); - if (Score.Files?.Count > 0) - items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score))); + if (Score.Files.Count > 0) + items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score))); if (Score.ID != 0) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 32307fc50e..e09cc7c9cd 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -15,6 +15,9 @@ namespace osu.Game.Online [Resolved(CanBeNull = true)] protected ScoreManager? Manager { get; private set; } + [Resolved(CanBeNull = true)] + protected ScoreModelDownloader? Downloader { get; private set; } + private ArchiveDownloadRequest? attachedRequest; public ScoreDownloadTracker(ScoreInfo trackedItem) @@ -25,7 +28,7 @@ namespace osu.Game.Online [BackgroundDependencyLoader(true)] private void load() { - if (Manager == null) + if (Manager == null || Downloader == null) return; // Used to interact with manager classes that don't support interface types. Will eventually be replaced. @@ -38,10 +41,10 @@ namespace osu.Game.Online if (Manager.IsAvailableLocally(scoreInfo)) UpdateState(DownloadState.LocallyAvailable); else - attachDownload(Manager.GetExistingDownload(scoreInfo)); + attachDownload(Downloader.GetExistingDownload(scoreInfo)); - Manager.DownloadBegan += downloadBegan; - Manager.DownloadFailed += downloadFailed; + Downloader.DownloadBegan += downloadBegan; + Downloader.DownloadFailed += downloadFailed; Manager.ItemUpdated += itemUpdated; Manager.ItemRemoved += itemRemoved; } @@ -119,10 +122,14 @@ namespace osu.Game.Online base.Dispose(isDisposing); attachDownload(null); + if (Downloader != null) + { + Downloader.DownloadBegan -= downloadBegan; + Downloader.DownloadFailed -= downloadFailed; + } + if (Manager != null) { - Manager.DownloadBegan -= downloadBegan; - Manager.DownloadFailed -= downloadFailed; Manager.ItemUpdated -= itemUpdated; Manager.ItemRemoved -= itemRemoved; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 574a5e5393..99b67976e3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -116,7 +116,7 @@ namespace osu.Game private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender(); [Cached] - private readonly StableImportManager stableImportManager = new StableImportManager(); + private readonly LegacyImportManager legacyImportManager = new LegacyImportManager(); [Cached] private readonly ScreenshotManager screenshotManager = new ScreenshotManager(); @@ -656,6 +656,9 @@ namespace osu.Game BeatmapManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value); + BeatmapDownloader.PostNotification = n => Notifications.Post(n); + ScoreDownloader.PostNotification = n => Notifications.Post(n); + ScoreManager.PostNotification = n => Notifications.Post(n); ScoreManager.PostImport = items => PresentScore(items.First().Value); @@ -782,7 +785,7 @@ namespace osu.Game PostNotification = n => Notifications.Post(n), }, Add, true); - loadComponentSingleFile(stableImportManager, Add); + loadComponentSingleFile(legacyImportManager, Add); loadComponentSingleFile(screenshotManager, Add); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dd4ae590c7..88c9ab370c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -96,8 +96,12 @@ namespace osu.Game protected BeatmapManager BeatmapManager { get; private set; } + protected BeatmapModelDownloader BeatmapDownloader { get; private set; } + protected ScoreManager ScoreManager { get; private set; } + protected ScoreModelDownloader ScoreDownloader { get; private set; } + protected SkinManager SkinManager { get; private set; } protected RulesetStore RulesetStore { get; private set; } @@ -232,9 +236,12 @@ namespace osu.Game dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); + dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); + dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API)); + // the following realm components are not actively used yet, but initialised and kept up to date for initial testing. realmRulesetStore = new RealmRulesetStore(realmFactory, Storage); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index fa57191ef3..38f2bdb34f 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -119,7 +119,7 @@ namespace osu.Game.Overlays.BeatmapListing [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - sortControlBackground.Colour = colourProvider.Background5; + sortControlBackground.Colour = colourProvider.Background4; } public void Search(string query) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs index 1282a14c3d..0a66c3ccb7 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs @@ -65,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig) + private void load(OsuGame game, BeatmapModelDownloader beatmaps, OsuConfigManager osuConfig) { noVideoSetting = osuConfig.GetBindable(OsuSetting.PreferNoVideo); diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index e08af52a72..49f2f5c211 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -15,12 +15,11 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Game.Audio; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapListing; -using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -34,7 +33,7 @@ namespace osu.Game.Overlays private Drawable currentContent; private Container panelTarget; - private FillFlowContainer foundContent; + private FillFlowContainer foundContent; private NotFoundDrawable notFoundContent; private SupporterRequiredDrawable supporterRequiredContent; private BeatmapListingFilterControl filterControl; @@ -69,7 +68,7 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background4, + Colour = ColourProvider.Background5, }, panelTarget = new Container { @@ -79,7 +78,7 @@ namespace osu.Game.Overlays Padding = new MarginPadding { Horizontal = 20 }, Children = new Drawable[] { - foundContent = new FillFlowContainer(), + foundContent = new FillFlowContainer(), notFoundContent = new NotFoundDrawable(), supporterRequiredContent = new SupporterRequiredDrawable(), } @@ -136,7 +135,7 @@ namespace osu.Game.Overlays return; } - var newPanels = searchResult.Results.Select(b => new GridBeatmapPanel(b) + var newPanels = searchResult.Results.Select(b => new BeatmapCard(b) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -152,7 +151,7 @@ namespace osu.Game.Overlays } // spawn new children with the contained so we only clear old content at the last moment. - var content = new FillFlowContainer + var content = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs index ee40f114d2..4eed8f28f2 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons } [BackgroundDependencyLoader] - private void load(IAPIProvider api, BeatmapManager beatmaps) + private void load(IAPIProvider api, BeatmapModelDownloader beatmaps) { FillFlowContainer textSprites; diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 36b94283e5..e46e503dfa 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -6,10 +6,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays.BeatmapListing.Panels; using osuTK; using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); protected override Drawable CreateDrawableItem(APIBeatmapSet model) => model.OnlineID > 0 - ? new GridBeatmapPanel(model) + ? new BeatmapCard(model) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index b95b0a1afc..cc553ad361 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -13,9 +13,9 @@ using osu.Game.Online.API.Requests; using osu.Game.Overlays.Rankings.Tables; using System.Linq; using System.Threading; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.BeatmapListing.Panels; namespace osu.Game.Overlays.Rankings { @@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Rankings AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Spacing = new Vector2(10), - Children = response.BeatmapSets.Select(b => new GridBeatmapPanel(b) + Children = response.BeatmapSets.Select(b => new BeatmapCard(b) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 43df58a8b1..5bc89ec77c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -31,9 +31,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private SettingsButton undeleteButton; [BackgroundDependencyLoader(permitNulls: true)] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay) + private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] LegacyImportManager legacyImportManager, DialogOverlay dialogOverlay) { - if (stableImportManager?.SupportsImportFromStable == true) + if (legacyImportManager?.SupportsImportFromStable == true) { Add(importBeatmapsButton = new SettingsButton { @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { importBeatmapsButton.Enabled.Value = false; - stableImportManager.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() => importBeatmapsButton.Enabled.Value = true)); + legacyImportManager.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() => importBeatmapsButton.Enabled.Value = true)); } }); } @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance } }); - if (stableImportManager?.SupportsImportFromStable == true) + if (legacyImportManager?.SupportsImportFromStable == true) { Add(importScoresButton = new SettingsButton { @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { importScoresButton.Enabled.Value = false; - stableImportManager.ImportFromStableAsync(StableContent.Scores).ContinueWith(t => Schedule(() => importScoresButton.Enabled.Value = true)); + legacyImportManager.ImportFromStableAsync(StableContent.Scores).ContinueWith(t => Schedule(() => importScoresButton.Enabled.Value = true)); } }); } @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance } }); - if (stableImportManager?.SupportsImportFromStable == true) + if (legacyImportManager?.SupportsImportFromStable == true) { Add(importSkinsButton = new SettingsButton { @@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { importSkinsButton.Enabled.Value = false; - stableImportManager.ImportFromStableAsync(StableContent.Skins).ContinueWith(t => Schedule(() => importSkinsButton.Enabled.Value = true)); + legacyImportManager.ImportFromStableAsync(StableContent.Skins).ContinueWith(t => Schedule(() => importSkinsButton.Enabled.Value = true)); } }); } @@ -113,7 +113,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance if (collectionManager != null) { - if (stableImportManager?.SupportsImportFromStable == true) + if (legacyImportManager?.SupportsImportFromStable == true) { Add(importCollectionsButton = new SettingsButton { @@ -121,7 +121,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { importCollectionsButton.Enabled.Value = false; - stableImportManager.ImportFromStableAsync(StableContent.Collections).ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); + legacyImportManager.ImportFromStableAsync(StableContent.Collections).ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); } }); } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index e7a7abed59..0eb65b4b0f 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -11,7 +11,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Skinning; @@ -167,6 +169,9 @@ namespace osu.Game.Overlays.Settings.Sections [Resolved] private SkinManager skins { get; set; } + [Resolved] + private Storage storage { get; set; } + private Bindable currentSkin; [BackgroundDependencyLoader] @@ -183,7 +188,7 @@ namespace osu.Game.Overlays.Settings.Sections { try { - skins.Export(currentSkin.Value.SkinInfo); + new LegacySkinExporter(storage).Export(currentSkin.Value.SkinInfo); } catch (Exception e) { diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index 21a402f8c3..8b5b228632 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets; namespace osu.Game.Scoring { - public interface IScoreInfo : IHasOnlineID + public interface IScoreInfo : IHasOnlineID, IHasNamedFiles { APIUser User { get; } diff --git a/osu.Game/Scoring/ScoreFileInfo.cs b/osu.Game/Scoring/ScoreFileInfo.cs index 9075fdec5b..b2e81d4b8d 100644 --- a/osu.Game/Scoring/ScoreFileInfo.cs +++ b/osu.Game/Scoring/ScoreFileInfo.cs @@ -7,7 +7,7 @@ using osu.Game.IO; namespace osu.Game.Scoring { - public class ScoreFileInfo : INamedFileInfo, IHasPrimaryKey + public class ScoreFileInfo : INamedFileInfo, IHasPrimaryKey, INamedFileUsage { public int ID { get; set; } @@ -17,5 +17,7 @@ namespace osu.Game.Scoring [Required] public string Filename { get; set; } + + IFileInfo INamedFileUsage.File => FileInfo; } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 736a939a59..564aa3b98c 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -160,7 +160,7 @@ namespace osu.Game.Scoring [NotMapped] public List HitEvents { get; set; } - public List Files { get; set; } + public List Files { get; } = new List(); public string Hash { get; set; } @@ -257,5 +257,7 @@ namespace osu.Game.Scoring bool IScoreInfo.HasReplay => Files.Any(); #endregion + + IEnumerable IHasNamedFiles.Files => Files; } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 29144e7bdc..e9cd44ae83 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -15,9 +14,7 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.IO; using osu.Game.IO.Archives; -using osu.Game.Online.API; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; @@ -25,15 +22,14 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring { - public class ScoreManager : IModelManager, IModelImporter, IModelFileManager, IModelDownloader + public class ScoreManager : IModelManager, IModelImporter { private readonly Scheduler scheduler; private readonly Func difficulties; private readonly OsuConfigManager configManager; private readonly ScoreModelManager scoreModelManager; - private readonly ScoreModelDownloader scoreModelDownloader; - public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler, + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IDatabaseContextFactory contextFactory, Scheduler scheduler, IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) { this.scheduler = scheduler; @@ -41,7 +37,6 @@ namespace osu.Game.Scoring this.configManager = configManager; scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory, importHost); - scoreModelDownloader = new ScoreModelDownloader(scoreModelManager, api, importHost); } public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score); @@ -240,11 +235,7 @@ namespace osu.Game.Scoring public Action PostNotification { - set - { - scoreModelManager.PostNotification = value; - scoreModelDownloader.PostNotification = value; - } + set => scoreModelManager.PostNotification = value; } #endregion @@ -263,21 +254,6 @@ namespace osu.Game.Scoring remove => scoreModelManager.ItemRemoved -= value; } - public Task ImportFromStableAsync(StableStorage stableStorage) - { - return scoreModelManager.ImportFromStableAsync(stableStorage); - } - - public void Export(ScoreInfo item) - { - scoreModelManager.Export(item); - } - - public void ExportModelTo(ScoreInfo model, Stream outputStream) - { - scoreModelManager.ExportModelTo(model, outputStream); - } - public void Update(ScoreInfo item) { scoreModelManager.Update(item); @@ -342,49 +318,6 @@ namespace osu.Game.Scoring #endregion - #region Implementation of IModelFileManager - - public void ReplaceFile(ScoreInfo model, ScoreFileInfo file, Stream contents, string filename = null) - { - scoreModelManager.ReplaceFile(model, file, contents, filename); - } - - public void DeleteFile(ScoreInfo model, ScoreFileInfo file) - { - scoreModelManager.DeleteFile(model, file); - } - - public void AddFile(ScoreInfo model, Stream contents, string filename) - { - scoreModelManager.AddFile(model, contents, filename); - } - - #endregion - - #region Implementation of IModelDownloader - - public event Action> DownloadBegan - { - add => scoreModelDownloader.DownloadBegan += value; - remove => scoreModelDownloader.DownloadBegan -= value; - } - - public event Action> DownloadFailed - { - add => scoreModelDownloader.DownloadFailed += value; - remove => scoreModelDownloader.DownloadFailed -= value; - } - - public bool Download(IScoreInfo model, bool minimiseDownloadSize) => - scoreModelDownloader.Download(model, minimiseDownloadSize); - - public ArchiveDownloadRequest GetExistingDownload(IScoreInfo model) - { - return scoreModelDownloader.GetExistingDownload(model); - } - - #endregion - #region Implementation of IPresentImports public Action>> PostImport diff --git a/osu.Game/Scoring/ScoreModelDownloader.cs b/osu.Game/Scoring/ScoreModelDownloader.cs index 6c63e2aa71..038a4bc351 100644 --- a/osu.Game/Scoring/ScoreModelDownloader.cs +++ b/osu.Game/Scoring/ScoreModelDownloader.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -10,8 +9,8 @@ namespace osu.Game.Scoring { public class ScoreModelDownloader : ModelDownloader { - public ScoreModelDownloader(IModelImporter scoreManager, IAPIProvider api, IIpcHost importHost = null) - : base(scoreManager, api, importHost) + public ScoreModelDownloader(IModelImporter scoreManager, IAPIProvider api) + : base(scoreManager, api) { } diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index c194a7166d..2cbd3aded7 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Linq.Expressions; using System.Threading; @@ -13,7 +12,6 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.IO.Archives; using osu.Game.Rulesets; using osu.Game.Scoring.Legacy; @@ -26,8 +24,6 @@ namespace osu.Game.Scoring protected override string[] HashableFileTypes => new[] { ".osr" }; - protected override string ImportFromStablePath => Path.Combine("Data", "r"); - private readonly RulesetStore rulesets; private readonly Func beatmaps; @@ -71,19 +67,5 @@ namespace osu.Game.Scoring protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) => base.CheckLocalAvailability(model, items) || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); - - public override void ExportModelTo(ScoreInfo model, Stream outputStream) - { - var file = model.Files.SingleOrDefault(); - if (file == null) - return; - - using (var inputStream = Files.Storage.GetStream(file.FileInfo.GetStoragePath())) - inputStream.CopyTo(outputStream); - } - - protected override IEnumerable GetStableImportPaths(Storage storage) - => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) - .Select(path => storage.GetFullPath(path)); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 94b6e58b67..ac71298f36 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -16,9 +16,11 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; @@ -63,6 +65,9 @@ namespace osu.Game.Screens.Edit [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved] + private Storage storage { get; set; } + [Resolved(canBeNull: true)] private DialogOverlay dialogOverlay { get; set; } @@ -753,7 +758,7 @@ namespace osu.Game.Screens.Edit private void exportBeatmap() { Save(); - beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); + new LegacyBeatmapExporter(storage).Export(Beatmap.Value.BeatmapSetInfo); } private void updateLastSavedHash() diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index dc04d9a77b..3592a51b4a 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -18,11 +18,15 @@ namespace osu.Game.Screens.OnlinePlay private readonly bool allowEdit; private readonly bool allowSelection; + private readonly bool showItemOwner; - public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool reverse = false) + public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool reverse = false, bool showItemOwner = false) { this.allowEdit = allowEdit; this.allowSelection = allowSelection; + this.showItemOwner = showItemOwner; + + ((ReversibleFillFlowContainer)ListContainer).Reverse = reverse; } protected override void LoadComplete() @@ -52,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay Spacing = new Vector2(0, 2) }; - protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item, allowEdit, allowSelection) + protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item, allowEdit, allowSelection, showItemOwner) { SelectedItem = { BindTarget = SelectedItem }, RequestDeletion = requestDeletion diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 5a86161b50..6cbdc80d60 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -4,17 +4,21 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -26,6 +30,7 @@ using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; +using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; @@ -34,6 +39,7 @@ namespace osu.Game.Screens.OnlinePlay public class DrawableRoomPlaylistItem : OsuRearrangeableListItem { public const float HEIGHT = 50; + public const float ICON_HEIGHT = 34; public Action RequestDeletion; @@ -45,6 +51,7 @@ namespace osu.Game.Screens.OnlinePlay private LinkFlowContainer authorText; private ExplicitContentBeatmapPill explicitContentPill; private ModDisplay modDisplay; + private UpdateableAvatar ownerAvatar; private readonly IBindable valid = new Bindable(); @@ -54,12 +61,19 @@ namespace osu.Game.Screens.OnlinePlay public readonly PlaylistItem Item; + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + private readonly bool allowEdit; private readonly bool allowSelection; + private readonly bool showItemOwner; protected override bool ShouldBeConsideredForInput(Drawable child) => allowEdit || !allowSelection || SelectedItem.Value == Model; - public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection) + public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection, bool showItemOwner) : base(item) { Item = item; @@ -67,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay // TODO: edit support should be moved out into a derived class this.allowEdit = allowEdit; this.allowSelection = allowSelection; + this.showItemOwner = showItemOwner; beatmap.BindTo(item.Beatmap); valid.BindTo(item.Valid); @@ -79,9 +94,6 @@ namespace osu.Game.Screens.OnlinePlay Colour = OsuColour.Gray(0.5f); } - [Resolved] - private OsuColour colours { get; set; } - [BackgroundDependencyLoader] private void load() { @@ -132,7 +144,13 @@ namespace osu.Game.Screens.OnlinePlay maskingContainer.BorderColour = colours.Red; } - difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(32) }; + if (showItemOwner) + { + ownerAvatar.Show(); + userLookupCache.GetUserAsync(Item.OwnerID).ContinueWith(u => Schedule(() => ownerAvatar.User = u.Result), TaskContinuationOptions.OnlyOnRanToCompletion); + } + + difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(ICON_HEIGHT) }; panelBackground.Beatmap.Value = Item.Beatmap.Value; @@ -186,6 +204,7 @@ namespace osu.Game.Screens.OnlinePlay new Dimension(GridSizeMode.AutoSize), new Dimension(), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) }, Content = new[] { @@ -196,7 +215,7 @@ namespace osu.Game.Screens.OnlinePlay Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Left = 8, Right = 8, }, + Margin = new MarginPadding { Left = 8, Right = 8 }, }, new FillFlowContainer { @@ -259,7 +278,7 @@ namespace osu.Game.Screens.OnlinePlay Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = 8, Right = 10, }, + Margin = new MarginPadding { Horizontal = 8 }, AutoSizeAxes = Axes.Both, Spacing = new Vector2(5), ChildrenEnumerable = CreateButtons().Select(button => button.With(b => @@ -267,7 +286,17 @@ namespace osu.Game.Screens.OnlinePlay b.Anchor = Anchor.Centre; b.Origin = Anchor.Centre; })) - } + }, + ownerAvatar = new OwnerAvatar + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(ICON_HEIGHT), + Margin = new MarginPadding { Right = 8 }, + Masking = true, + CornerRadius = 4, + Alpha = showItemOwner ? 1 : 0 + }, } } }, @@ -417,5 +446,31 @@ namespace osu.Game.Screens.OnlinePlay Beatmap.BindValueChanged(beatmap => backgroundSprite.Beatmap.Value = beatmap.NewValue); } } + + private class OwnerAvatar : UpdateableAvatar, IHasTooltip + { + public OwnerAvatar() + { + AddInternal(new TooltipArea(this) + { + RelativeSizeAxes = Axes.Both, + Depth = -1 + }); + } + + public LocalisableString TooltipText => User == null ? "loading user..." : $"queued by {User.Username}"; + + private class TooltipArea : Component, IHasTooltip + { + private readonly OwnerAvatar avatar; + + public TooltipArea(OwnerAvatar avatar) + { + this.avatar = avatar; + } + + public LocalisableString TooltipText => avatar.TooltipText; + } + } } } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs index 575f336e58..8b1bb7abc1 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs @@ -19,13 +19,16 @@ namespace osu.Game.Screens.OnlinePlay { public Action RequestShowResults; - public DrawableRoomPlaylistWithResults() - : base(false, true) + private readonly bool showItemOwner; + + public DrawableRoomPlaylistWithResults(bool showItemOwner = false) + : base(false, true, showItemOwner: showItemOwner) { + this.showItemOwner = showItemOwner; } protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => - new DrawableRoomPlaylistItemWithResults(item, false, true) + new DrawableRoomPlaylistItemWithResults(item, false, true, showItemOwner) { RequestShowResults = () => RequestShowResults(item), SelectedItem = { BindTarget = SelectedItem }, @@ -35,8 +38,8 @@ namespace osu.Game.Screens.OnlinePlay { public Action RequestShowResults; - public DrawableRoomPlaylistItemWithResults(PlaylistItem item, bool allowEdit, bool allowSelection) - : base(item, allowEdit, allowSelection) + public DrawableRoomPlaylistItemWithResults(PlaylistItem item, bool allowEdit, bool allowSelection, bool showItemOwner) + : base(item, allowEdit, allowSelection, showItemOwner) { } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 323d38c881..7c5ed3f5cc 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -302,6 +302,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override void OnResuming(IScreen last) { base.OnResuming(last); + updateWorkingBeatmap(); beginHandlingTrack(); Scheduler.AddOnce(UpdateMods); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 1e3cfdbcbb..077e9cef93 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -153,7 +153,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer null, new Drawable[] { - playlist = new DrawableRoomPlaylist(false, false, true) + playlist = new DrawableRoomPlaylist(false, false, true, true) { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index c65d4af2ae..45601999a0 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -12,6 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -20,7 +21,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; -using osu.Game.Overlays.BeatmapListing.Panels; +using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -49,6 +50,12 @@ namespace osu.Game.Screens.Play [Resolved] private BeatmapManager beatmaps { get; set; } + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + private Container beatmapPanelContainer; private TriangleButton watchButton; private SettingsCheckbox automaticDownload; @@ -70,7 +77,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager config) + private void load(OsuConfigManager config) { InternalChild = new Container { @@ -85,7 +92,7 @@ namespace osu.Game.Screens.Play { new Box { - Colour = colours.GreySeafoamDark, + Colour = colourProvider.Background5, RelativeSizeAxes = Axes.Both, }, new FillFlowContainer @@ -226,7 +233,7 @@ namespace osu.Game.Screens.Play onlineBeatmapRequest.Success += beatmapSet => Schedule(() => { this.beatmapSet = beatmapSet; - beatmapPanelContainer.Child = new GridBeatmapPanel(this.beatmapSet); + beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet); checkForAutomaticDownload(); }); @@ -244,7 +251,7 @@ namespace osu.Game.Screens.Play if (beatmaps.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID })) return; - beatmaps.Download(beatmapSet); + beatmapDownloader.Download(beatmapSet); } public override bool OnExiting(IScreen next) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 66b3c973f5..6a74fdaf75 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, ScoreManager scores) + private void load(OsuGame game, ScoreModelDownloader scores) { InternalChild = shakeContainer = new ShakeContainer { @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Ranking break; case DownloadState.NotDownloaded: - scores.Download(Score.Value, false); + scores.Download(Score.Value); break; case DownloadState.Importing: diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2c36bf5fc8..f715c7ff59 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Select protected virtual bool ShowFooter => true; - protected virtual bool DisplayStableImportPrompt => stableImportManager?.SupportsImportFromStable == true; + protected virtual bool DisplayStableImportPrompt => legacyImportManager?.SupportsImportFromStable == true; public override bool? AllowTrackAdjustments => true; @@ -76,6 +76,8 @@ namespace osu.Game.Screens.Select /// public virtual bool AllowEditing => true; + public bool BeatmapSetsLoaded => IsLoaded && Carousel?.BeatmapSetsLoaded == true; + [Resolved] private Bindable> selectedMods { get; set; } @@ -90,7 +92,7 @@ namespace osu.Game.Screens.Select private BeatmapManager beatmaps { get; set; } [Resolved(CanBeNull = true)] - private StableImportManager stableImportManager { get; set; } + private LegacyImportManager legacyImportManager { get; set; } protected ModSelectOverlay ModSelect { get; private set; } @@ -297,7 +299,7 @@ namespace osu.Game.Screens.Select { dialogOverlay.Push(new ImportFromStablePopup(() => { - Task.Run(() => stableImportManager.ImportFromStableAsync(StableContent.All)); + Task.Run(() => legacyImportManager.ImportFromStableAsync(StableContent.All)); })); } }); diff --git a/osu.Game/Skinning/LegacySkinResourceStore.cs b/osu.Game/Skinning/LegacySkinResourceStore.cs index 096b467867..c4418baeff 100644 --- a/osu.Game/Skinning/LegacySkinResourceStore.cs +++ b/osu.Game/Skinning/LegacySkinResourceStore.cs @@ -24,9 +24,6 @@ namespace osu.Game.Skinning protected override IEnumerable GetFilenames(string name) { - if (source.Files == null) - yield break; - foreach (string filename in base.GetFilenames(name)) { string path = getPathForFile(filename.ToStandardisedPath()); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index fae1a599d1..10526b69af 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -59,7 +59,7 @@ namespace osu.Game.Skinning string filename = $"{skinnableTarget}.json"; // skininfo files may be null for default skin. - var fileInfo = SkinInfo.Files?.FirstOrDefault(f => f.Filename == filename); + var fileInfo = SkinInfo.Files.FirstOrDefault(f => f.Filename == filename); if (fileInfo == null) continue; diff --git a/osu.Game/Skinning/SkinFileInfo.cs b/osu.Game/Skinning/SkinFileInfo.cs index 8a7019e1a3..db7cd953bb 100644 --- a/osu.Game/Skinning/SkinFileInfo.cs +++ b/osu.Game/Skinning/SkinFileInfo.cs @@ -7,7 +7,7 @@ using osu.Game.IO; namespace osu.Game.Skinning { - public class SkinFileInfo : INamedFileInfo, IHasPrimaryKey + public class SkinFileInfo : INamedFileInfo, IHasPrimaryKey, INamedFileUsage { public int ID { get; set; } @@ -19,5 +19,7 @@ namespace osu.Game.Skinning [Required] public string Filename { get; set; } + + IFileInfo INamedFileUsage.File => FileInfo; } } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 3b34e23d57..5d2d51a9b0 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -10,7 +10,7 @@ using osu.Game.IO; namespace osu.Game.Skinning { - public class SkinInfo : IHasFiles, IEquatable, IHasPrimaryKey, ISoftDelete + public class SkinInfo : IHasFiles, IEquatable, IHasPrimaryKey, ISoftDelete, IHasNamedFiles { internal const int DEFAULT_SKIN = 0; internal const int CLASSIC_SKIN = -1; @@ -36,7 +36,7 @@ namespace osu.Game.Skinning return (Skin)Activator.CreateInstance(type, this, resources); } - public List Files { get; set; } = new List(); + public List Files { get; } = new List(); public bool DeletePending { get; set; } @@ -55,5 +55,7 @@ namespace osu.Game.Skinning string author = Creator == null ? string.Empty : $"({Creator})"; return $"{Name} {author}".Trim(); } + + IEnumerable IHasNamedFiles.Files => Files; } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 6e0a8e0a11..8d07dd046a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -18,15 +18,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Overlays.Notifications; namespace osu.Game.Skinning { @@ -38,7 +37,7 @@ namespace osu.Game.Skinning /// For gameplay components, see which adds extra legacy and toggle logic that may affect the lookup process. /// [ExcludeFromDynamicCompile] - public class SkinManager : ArchiveModelManager, ISkinSource, IStorageResourceProvider + public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter, IModelManager { private readonly AudioManager audio; @@ -49,11 +48,11 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(); public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; - public override IEnumerable HandledExtensions => new[] { ".osk" }; + private readonly SkinModelManager skinModelManager; - protected override string[] HashableFileTypes => new[] { ".ini", ".json" }; + private readonly SkinStore skinStore; - protected override string ImportFromStablePath => "Skins"; + private readonly IResourceStore userFiles; /// /// The default skin. @@ -66,12 +65,16 @@ namespace osu.Game.Skinning public Skin DefaultLegacySkin { get; } public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore resources, AudioManager audio) - : base(storage, contextFactory, new SkinStore(contextFactory, storage), host) { this.audio = audio; this.host = host; this.resources = resources; + skinStore = new SkinStore(contextFactory, storage); + userFiles = new FileStore(contextFactory, storage).Store; + + skinModelManager = new SkinModelManager(storage, contextFactory, skinStore, host, this); + DefaultLegacySkin = new DefaultLegacySkin(this); DefaultSkin = new DefaultSkin(this); @@ -85,31 +88,8 @@ namespace osu.Game.Skinning SourceChanged?.Invoke(); }; - - // can be removed 20220420. - populateMissingHashes(); } - private void populateMissingHashes() - { - var skinsWithoutHashes = ModelStore.ConsumableItems.Where(i => i.Hash == null).ToArray(); - - foreach (SkinInfo skin in skinsWithoutHashes) - { - try - { - Update(skin); - } - catch (Exception e) - { - Delete(skin); - Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid"); - } - } - } - - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == @".osk"; - /// /// Returns a list of all usable s. Includes the special default skin plus all skins from . /// @@ -129,15 +109,15 @@ namespace osu.Game.Skinning public List GetAllUserSkins(bool includeFiles = false) { if (includeFiles) - return ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + return skinStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); - return ModelStore.Items.Where(s => !s.DeletePending).ToList(); + return skinStore.Items.Where(s => !s.DeletePending).ToList(); } public void SelectRandomSkin() { // choose from only user skins, removing the current selection to ensure a new one is chosen. - var randomChoices = ModelStore.Items.Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); + var randomChoices = skinStore.Items.Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); if (randomChoices.Length == 0) { @@ -146,137 +126,7 @@ namespace osu.Game.Skinning } var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); - CurrentSkinInfo.Value = ModelStore.ConsumableItems.Single(i => i.ID == chosen.ID); - } - - protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name ?? @"No name" }; - - private const string unknown_creator_string = @"Unknown"; - - protected override bool HasCustomHashFunction => true; - - protected override string ComputeHash(SkinInfo item) - { - var instance = GetSkin(item); - - // This function can be run on fresh import or save. The logic here ensures a skin.ini file is in a good state for both operations. - - // `Skin` will parse the skin.ini and populate `Skin.Configuration` during construction above. - string skinIniSourcedName = instance.Configuration.SkinInfo.Name; - string skinIniSourcedCreator = instance.Configuration.SkinInfo.Creator; - string archiveName = item.Name.Replace(@".osk", string.Empty, StringComparison.OrdinalIgnoreCase); - - bool isImport = item.ID == 0; - - if (isImport) - { - item.Name = !string.IsNullOrEmpty(skinIniSourcedName) ? skinIniSourcedName : archiveName; - item.Creator = !string.IsNullOrEmpty(skinIniSourcedCreator) ? skinIniSourcedCreator : unknown_creator_string; - - // For imports, we want to use the archive or folder name as part of the metadata, in addition to any existing skin.ini metadata. - // In an ideal world, skin.ini would be the only source of metadata, but a lot of skin creators and users don't update it when making modifications. - // In both of these cases, the expectation from the user is that the filename or folder name is displayed somewhere to identify the skin. - if (archiveName != item.Name) - item.Name = @$"{item.Name} [{archiveName}]"; - } - - // By this point, the metadata in SkinInfo will be correct. - // Regardless of whether this is an import or not, let's write the skin.ini if non-existing or non-matching. - // This is (weirdly) done inside ComputeHash to avoid adding a new method to handle this case. After switching to realm it can be moved into another place. - if (skinIniSourcedName != item.Name) - updateSkinIniMetadata(item); - - return base.ComputeHash(item); - } - - private void updateSkinIniMetadata(SkinInfo item) - { - string nameLine = @$"Name: {item.Name}"; - string authorLine = @$"Author: {item.Creator}"; - - string[] newLines = - { - @"// The following content was automatically added by osu! during import, based on filename / folder metadata.", - @"[General]", - nameLine, - authorLine, - }; - - var existingFile = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase)); - - if (existingFile == null) - { - // In the case a skin doesn't have a skin.ini yet, let's create one. - writeNewSkinIni(); - return; - } - - using (Stream stream = new MemoryStream()) - { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - { - using (var existingStream = Files.Storage.GetStream(existingFile.FileInfo.GetStoragePath())) - using (var sr = new StreamReader(existingStream)) - { - string line; - while ((line = sr.ReadLine()) != null) - sw.WriteLine(line); - } - - sw.WriteLine(); - - foreach (string line in newLines) - sw.WriteLine(line); - } - - ReplaceFile(item, existingFile, stream); - - // can be removed 20220502. - if (!ensureIniWasUpdated(item)) - { - Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important); - - DeleteFile(item, item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))); - writeNewSkinIni(); - } - } - - void writeNewSkinIni() - { - using (Stream stream = new MemoryStream()) - { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - { - foreach (string line in newLines) - sw.WriteLine(line); - } - - AddFile(item, stream, @"skin.ini"); - } - } - } - - private bool ensureIniWasUpdated(SkinInfo item) - { - // This is a final consistency check to ensure that hash computation doesn't enter an infinite loop. - // With other changes to the surrounding code this should never be hit, but until we are 101% sure that there - // are no other cases let's avoid a hard startup crash by bailing and alerting. - - var instance = GetSkin(item); - - return instance.Configuration.SkinInfo.Name == item.Name; - } - - protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) - { - var instance = GetSkin(model); - - model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo(); - - model.Name = instance.Configuration.SkinInfo.Name; - model.Creator = instance.Configuration.SkinInfo.Creator; - - return Task.CompletedTask; + CurrentSkinInfo.Value = skinStore.ConsumableItems.Single(i => i.ID == chosen.ID); } /// @@ -297,7 +147,7 @@ namespace osu.Game.Skinning var skin = CurrentSkin.Value; // if the user is attempting to save one of the default skin implementations, create a copy first. - CurrentSkinInfo.Value = Import(new SkinInfo + CurrentSkinInfo.Value = skinModelManager.Import(new SkinInfo { Name = skin.SkinInfo.Name + @" (modified)", Creator = skin.SkinInfo.Creator, @@ -321,9 +171,9 @@ namespace osu.Game.Skinning var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename); if (oldFile != null) - ReplaceFile(skin.SkinInfo, oldFile, streamContent, oldFile.Filename); + skinModelManager.ReplaceFile(skin.SkinInfo, oldFile, streamContent, oldFile.Filename); else - AddFile(skin.SkinInfo, streamContent, filename); + skinModelManager.AddFile(skin.SkinInfo, streamContent, filename); } } } @@ -333,7 +183,7 @@ namespace osu.Game.Skinning /// /// The query. /// The first result for the provided query, or null if no results were found. - public SkinInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + public SkinInfo Query(Expression> query) => skinStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); public event Action SourceChanged; @@ -386,9 +236,101 @@ namespace osu.Game.Skinning AudioManager IStorageResourceProvider.AudioManager => audio; IResourceStore IStorageResourceProvider.Resources => resources; - IResourceStore IStorageResourceProvider.Files => Files.Store; + IResourceStore IStorageResourceProvider.Files => userFiles; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); #endregion + + #region Implementation of IModelImporter + + public Action PostNotification + { + set => skinModelManager.PostNotification = value; + } + + public Action>> PostImport + { + set => skinModelManager.PostImport = value; + } + + public Task Import(params string[] paths) + { + return skinModelManager.Import(paths); + } + + public Task Import(params ImportTask[] tasks) + { + return skinModelManager.Import(tasks); + } + + public IEnumerable HandledExtensions => skinModelManager.HandledExtensions; + + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) + { + return skinModelManager.Import(notification, tasks); + } + + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return skinModelManager.Import(task, lowPriority, cancellationToken); + } + + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return skinModelManager.Import(archive, lowPriority, cancellationToken); + } + + public Task> Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return skinModelManager.Import(item, archive, lowPriority, cancellationToken); + } + + #endregion + + #region Implementation of IModelManager + + public event Action ItemUpdated + { + add => skinModelManager.ItemUpdated += value; + remove => skinModelManager.ItemUpdated -= value; + } + + public event Action ItemRemoved + { + add => skinModelManager.ItemRemoved += value; + remove => skinModelManager.ItemRemoved -= value; + } + + public void Update(SkinInfo item) + { + skinModelManager.Update(item); + } + + public bool Delete(SkinInfo item) + { + return skinModelManager.Delete(item); + } + + public void Delete(List items, bool silent = false) + { + skinModelManager.Delete(items, silent); + } + + public void Undelete(List items, bool silent = false) + { + skinModelManager.Undelete(items, silent); + } + + public void Undelete(SkinInfo item) + { + skinModelManager.Undelete(item); + } + + public bool IsAvailableLocally(SkinInfo model) + { + return skinModelManager.IsAvailableLocally(model); + } + + #endregion } } diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs new file mode 100644 index 0000000000..572ae5cbfc --- /dev/null +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -0,0 +1,189 @@ +// Copyright (c) ppy Pty Ltd . 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.Text; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.IO; +using osu.Game.IO.Archives; + +namespace osu.Game.Skinning +{ + public class SkinModelManager : ArchiveModelManager + { + private readonly IStorageResourceProvider skinResources; + + public SkinModelManager(Storage storage, DatabaseContextFactory contextFactory, SkinStore skinStore, GameHost host, IStorageResourceProvider skinResources) + : base(storage, contextFactory, skinStore, host) + { + this.skinResources = skinResources; + + // can be removed 20220420. + populateMissingHashes(); + } + + public override IEnumerable HandledExtensions => new[] { ".osk" }; + + protected override string[] HashableFileTypes => new[] { ".ini", ".json" }; + + protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == @".osk"; + + protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name ?? @"No name" }; + + private const string unknown_creator_string = @"Unknown"; + + protected override bool HasCustomHashFunction => true; + + protected override string ComputeHash(SkinInfo item) + { + var instance = createInstance(item); + + // This function can be run on fresh import or save. The logic here ensures a skin.ini file is in a good state for both operations. + + // `Skin` will parse the skin.ini and populate `Skin.Configuration` during construction above. + string skinIniSourcedName = instance.Configuration.SkinInfo.Name; + string skinIniSourcedCreator = instance.Configuration.SkinInfo.Creator; + string archiveName = item.Name.Replace(@".osk", string.Empty, StringComparison.OrdinalIgnoreCase); + + bool isImport = item.ID == 0; + + if (isImport) + { + item.Name = !string.IsNullOrEmpty(skinIniSourcedName) ? skinIniSourcedName : archiveName; + item.Creator = !string.IsNullOrEmpty(skinIniSourcedCreator) ? skinIniSourcedCreator : unknown_creator_string; + + // For imports, we want to use the archive or folder name as part of the metadata, in addition to any existing skin.ini metadata. + // In an ideal world, skin.ini would be the only source of metadata, but a lot of skin creators and users don't update it when making modifications. + // In both of these cases, the expectation from the user is that the filename or folder name is displayed somewhere to identify the skin. + if (archiveName != item.Name) + item.Name = @$"{item.Name} [{archiveName}]"; + } + + // By this point, the metadata in SkinInfo will be correct. + // Regardless of whether this is an import or not, let's write the skin.ini if non-existing or non-matching. + // This is (weirdly) done inside ComputeHash to avoid adding a new method to handle this case. After switching to realm it can be moved into another place. + if (skinIniSourcedName != item.Name) + updateSkinIniMetadata(item); + + return base.ComputeHash(item); + } + + private void updateSkinIniMetadata(SkinInfo item) + { + string nameLine = @$"Name: {item.Name}"; + string authorLine = @$"Author: {item.Creator}"; + + string[] newLines = + { + @"// The following content was automatically added by osu! during import, based on filename / folder metadata.", + @"[General]", + nameLine, + authorLine, + }; + + var existingFile = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase)); + + if (existingFile == null) + { + // In the case a skin doesn't have a skin.ini yet, let's create one. + writeNewSkinIni(); + return; + } + + using (Stream stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + { + using (var existingStream = Files.Storage.GetStream(existingFile.FileInfo.GetStoragePath())) + using (var sr = new StreamReader(existingStream)) + { + string line; + while ((line = sr.ReadLine()) != null) + sw.WriteLine(line); + } + + sw.WriteLine(); + + foreach (string line in newLines) + sw.WriteLine(line); + } + + ReplaceFile(item, existingFile, stream); + + // can be removed 20220502. + if (!ensureIniWasUpdated(item)) + { + Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important); + + DeleteFile(item, item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase))); + writeNewSkinIni(); + } + } + + void writeNewSkinIni() + { + using (Stream stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + { + foreach (string line in newLines) + sw.WriteLine(line); + } + + AddFile(item, stream, @"skin.ini"); + } + } + } + + private bool ensureIniWasUpdated(SkinInfo item) + { + // This is a final consistency check to ensure that hash computation doesn't enter an infinite loop. + // With other changes to the surrounding code this should never be hit, but until we are 101% sure that there + // are no other cases let's avoid a hard startup crash by bailing and alerting. + + var instance = createInstance(item); + + return instance.Configuration.SkinInfo.Name == item.Name; + } + + protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + { + var instance = createInstance(model); + + model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo(); + + model.Name = instance.Configuration.SkinInfo.Name; + model.Creator = instance.Configuration.SkinInfo.Creator; + + return Task.CompletedTask; + } + + private void populateMissingHashes() + { + var skinsWithoutHashes = ModelStore.ConsumableItems.Where(i => i.Hash == null).ToArray(); + + foreach (SkinInfo skin in skinsWithoutHashes) + { + try + { + Update(skin); + } + catch (Exception e) + { + Delete(skin); + Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid"); + } + } + } + + private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources); + } +} diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 6370d4ebe4..b74670e722 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -253,7 +253,7 @@ namespace osu.Game.Stores var scheduledImport = Task.Factory.StartNew(async () => await Import(model, archive, lowPriority, cancellationToken).ConfigureAwait(false), cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap(); - return await scheduledImport.ConfigureAwait(true); + return await scheduledImport.ConfigureAwait(false); } /// diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index aebf330443..f9abbda4c0 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -86,9 +86,13 @@ namespace osu.Game.Stores public void Cleanup() { - var realm = realmFactory.Context; + Logger.Log(@"Beginning realm file store cleanup"); + + int totalFiles = 0; + int removedFiles = 0; // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. + using (var realm = realmFactory.CreateContext()) using (var transaction = realm.BeginWrite()) { // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) @@ -96,11 +100,14 @@ namespace osu.Game.Stores foreach (var file in files) { + totalFiles++; + if (file.BacklinksCount > 0) continue; try { + removedFiles++; Storage.Delete(file.GetStoragePath()); realm.Remove(file); } @@ -112,6 +119,8 @@ namespace osu.Game.Stores transaction.Commit(); } + + Logger.Log($@"Finished realm file store cleanup ({removedFiles} of {totalFiles} deleted)"); } } } diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index adb447c927..9d92f5c5fc 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -87,23 +87,19 @@ namespace osu.Game.Tests.Beatmaps { AddStep("setup skins", () => { - userSkinInfo.Files = new List + userSkinInfo.Files.Clear(); + userSkinInfo.Files.Add(new SkinFileInfo { - new SkinFileInfo - { - Filename = userFile, - FileInfo = new IO.FileInfo { Hash = userFile } - } - }; + Filename = userFile, + FileInfo = new IO.FileInfo { Hash = userFile } + }); - beatmapInfo.BeatmapSet.Files = new List + beatmapInfo.BeatmapSet.Files.Clear(); + beatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo { - new BeatmapSetFileInfo - { - Filename = beatmapFile, - FileInfo = new IO.FileInfo { Hash = beatmapFile } - } - }; + Filename = beatmapFile, + FileInfo = new IO.FileInfo { Hash = beatmapFile } + }); // Need to refresh the cached skin source to refresh the skin resource store. dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this)); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b54e243bde..2d77e17513 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -299,7 +299,7 @@ namespace osu.Game.Tests.Visual.Multiplayer return ((IMultiplayerClient)this).LoadRequested(); } - public override async Task AddPlaylistItem(MultiplayerPlaylistItem item) + public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item) { Debug.Assert(Room != null); Debug.Assert(APIRoom != null); @@ -313,6 +313,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case QueueMode.HostOnly: // In host-only mode, the current item is re-used. item.ID = currentItem.ID; + item.OwnerID = currentItem.OwnerID; serverSidePlaylist[currentIndex] = item; await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); @@ -323,6 +324,7 @@ namespace osu.Game.Tests.Visual.Multiplayer default: item.ID = serverSidePlaylist.Last().ID + 1; + item.OwnerID = userId; serverSidePlaylist.Add(item); await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); @@ -332,6 +334,8 @@ namespace osu.Game.Tests.Visual.Multiplayer } } + public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item); + protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) { IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index f8419b4164..a1f010f082 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; @@ -55,6 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// Adds a room to a local "server-side" list that's returned when a is fired. /// /// The room. - public void AddServerSideRoom(Room room) => requestsHandler.AddServerSideRoom(room); + /// The host. + public void AddServerSideRoom(Room room, APIUser host) => requestsHandler.AddServerSideRoom(room, host); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index dc12e48297..abcf31c007 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value); apiRoom.Password.Value = createRoomRequest.Room.Password.Value; - AddServerSideRoom(apiRoom); + AddServerSideRoom(apiRoom, localUser); var responseRoom = new APICreatedRoom(); responseRoom.CopyFrom(createResponseRoom(apiRoom, false)); @@ -125,11 +125,17 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// Adds a room to a local "server-side" list that's returned when a is fired. /// /// The room. - public void AddServerSideRoom(Room room) + /// The room host. + public void AddServerSideRoom(Room room, APIUser host) { room.RoomID.Value ??= currentRoomId++; + room.Host.Value = host; + for (int i = 0; i < room.Playlist.Count; i++) + { room.Playlist[i].ID = currentPlaylistItemId++; + room.Playlist[i].OwnerID = room.Host.Value.OnlineID; + } serverSideRooms.Add(room); }