From a0842838e7aa880a8ce34621b2a0e77178457667 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 12 Jan 2022 00:15:17 +0100 Subject: [PATCH 001/227] Add `AllowIme => false` where applicable Also adds `AllowWordNavigation => false` to password text box. --- osu.Game/Graphics/UserInterface/OsuNumberBox.cs | 2 ++ osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 4 ++++ osu.Game/Overlays/Settings/SettingsNumberBox.cs | 2 ++ osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs | 2 ++ 4 files changed, 10 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs index 3d565a4464..8a3b77d3c2 100644 --- a/osu.Game/Graphics/UserInterface/OsuNumberBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuNumberBox.cs @@ -7,6 +7,8 @@ namespace osu.Game.Graphics.UserInterface { public class OsuNumberBox : OsuTextBox { + protected override bool AllowIme => false; + protected override bool CanAddCharacter(char character) => character.IsAsciiDigit(); } } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 8e82f4a7c1..b276159558 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -29,6 +29,10 @@ namespace osu.Game.Graphics.UserInterface protected override bool AllowClipboardExport => false; + protected override bool AllowWordNavigation => false; + + protected override bool AllowIme => false; + private readonly CapsWarning warning; [Resolved] diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index cc4446033a..d931c53e73 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -68,6 +68,8 @@ namespace osu.Game.Overlays.Settings private class OutlinedNumberBox : OutlinedTextBox { + protected override bool AllowIme => false; + protected override bool CanAddCharacter(char character) => character.IsAsciiDigit(); public new void NotifyInputError() => base.NotifyInputError(); diff --git a/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs b/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs index ee9d86029e..c39b4d6f41 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledRomanisedTextBox.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens.Edit.Setup private class RomanisedTextBox : OsuTextBox { + protected override bool AllowIme => false; + protected override bool CanAddCharacter(char character) => MetadataUtils.IsRomanised(character); } From c383f26729e3632a9d1e0567234435249652facc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 15:59:12 +0900 Subject: [PATCH 002/227] Remove EF specific tests that have since been replaced --- .../Beatmaps/IO/ImportBeatmapTest.cs | 975 +----------------- 1 file changed, 3 insertions(+), 972 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index c02141bf9f..da414dcf8d 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -2,983 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.IO; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Platform; -using osu.Game.IPC; using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Extensions; -using osu.Game.IO; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays.Notifications; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Scoring; using osu.Game.Tests.Resources; -using osu.Game.Tests.Scores.IO; -using SharpCompress.Archives; -using SharpCompress.Archives.Zip; -using SharpCompress.Common; -using SharpCompress.Writers.Zip; -using FileInfo = System.IO.FileInfo; namespace osu.Game.Tests.Beatmaps.IO { [TestFixture] public class ImportBeatmapTest : ImportTest { - [Test] - public async Task TestImportWhenClosed() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - await LoadOszIntoOsu(LoadOsuIntoHost(host)); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportThenDelete() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - var imported = await LoadOszIntoOsu(osu); - - deleteBeatmapSet(imported, osu); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportThenDeleteFromStream() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - string tempPath = TestResources.GetTestBeatmapForImport(); - - var manager = osu.Dependencies.Get(); - - ILive importedSet; - - using (var stream = File.OpenRead(tempPath)) - { - importedSet = await manager.Import(new ImportTask(stream, Path.GetFileName(tempPath))); - await ensureLoaded(osu); - } - - Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); - File.Delete(tempPath); - - var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); - - deleteBeatmapSet(imported, osu); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportThenImport() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - var imported = await LoadOszIntoOsu(osu); - var importedSecondTime = await LoadOszIntoOsu(osu); - - // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. - Assert.IsTrue(imported.ID == importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - - checkBeatmapSetCount(osu, 1); - checkSingleReferencedFileCount(osu, 18); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportThenImportWithReZip() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - string temp = TestResources.GetTestBeatmapForImport(); - - string extractedFolder = $"{temp}_extracted"; - Directory.CreateDirectory(extractedFolder); - - try - { - var imported = await LoadOszIntoOsu(osu); - - string hashBefore = hashFile(temp); - - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(extractedFolder); - - using (var zip = ZipArchive.Create()) - { - zip.AddAllFromDirectory(extractedFolder); - zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); - } - - // zip files differ because different compression or encoder. - Assert.AreNotEqual(hashBefore, hashFile(temp)); - - var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); - - await ensureLoaded(osu); - - // but contents doesn't, so existing should still be used. - Assert.IsTrue(imported.ID == importedSecondTime.Value.ID); - Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Value.Beatmaps.First().ID); - } - finally - { - Directory.Delete(extractedFolder, true); - } - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportThenImportWithChangedHashedFile() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - string temp = TestResources.GetTestBeatmapForImport(); - - string extractedFolder = $"{temp}_extracted"; - Directory.CreateDirectory(extractedFolder); - - try - { - var imported = await LoadOszIntoOsu(osu); - - await createScoreForBeatmap(osu, imported.Beatmaps.First()); - - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(extractedFolder); - - // arbitrary write to hashed file - // this triggers the special BeatmapManager.PreImport deletion/replacement flow. - using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText()) - await sw.WriteLineAsync("// changed"); - - using (var zip = ZipArchive.Create()) - { - zip.AddAllFromDirectory(extractedFolder); - zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); - } - - var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); - - await ensureLoaded(osu); - - // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); - } - finally - { - Directory.Delete(extractedFolder, true); - } - } - finally - { - host.Exit(); - } - } - } - - [Test] - [Ignore("intentionally broken by import optimisations")] - public async Task TestImportThenImportWithChangedFile() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - string temp = TestResources.GetTestBeatmapForImport(); - - string extractedFolder = $"{temp}_extracted"; - Directory.CreateDirectory(extractedFolder); - - try - { - var imported = await LoadOszIntoOsu(osu); - - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(extractedFolder); - - // arbitrary write to non-hashed file - using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText()) - await sw.WriteLineAsync("text"); - - using (var zip = ZipArchive.Create()) - { - zip.AddAllFromDirectory(extractedFolder); - zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); - } - - var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); - - await ensureLoaded(osu); - - // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); - } - finally - { - Directory.Delete(extractedFolder, true); - } - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportThenImportWithDifferentFilename() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - string temp = TestResources.GetTestBeatmapForImport(); - - string extractedFolder = $"{temp}_extracted"; - Directory.CreateDirectory(extractedFolder); - - try - { - var imported = await LoadOszIntoOsu(osu); - - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(extractedFolder); - - // change filename - var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First()); - firstFile.MoveTo(Path.Combine(firstFile.DirectoryName.AsNonNull(), $"{firstFile.Name}-changed{firstFile.Extension}")); - - using (var zip = ZipArchive.Create()) - { - zip.AddAllFromDirectory(extractedFolder); - zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); - } - - var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); - - await ensureLoaded(osu); - - // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); - } - finally - { - Directory.Delete(extractedFolder, true); - } - } - finally - { - host.Exit(); - } - } - } - - [Test] - [Ignore("intentionally broken by import optimisations")] - public async Task TestImportCorruptThenImport() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - var imported = await LoadOszIntoOsu(osu); - - var firstFile = imported.Files.First(); - - var files = osu.Dependencies.Get(); - - long originalLength; - using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath())) - originalLength = stream.Length; - - using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath(), FileAccess.Write, FileMode.Create)) - stream.WriteByte(0); - - var importedSecondTime = await LoadOszIntoOsu(osu); - - using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath())) - Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import"); - - // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. - Assert.IsTrue(imported.ID == importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - - checkBeatmapSetCount(osu, 1); - checkSingleReferencedFileCount(osu, 18); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestModelCreationFailureDoesntReturn() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - var importer = osu.Dependencies.Get(); - - var progressNotification = new ImportProgressNotification(); - - var zipStream = new MemoryStream(); - - using (var zip = ZipArchive.Create()) - zip.SaveTo(zipStream, new ZipWriterOptions(CompressionType.Deflate)); - - var imported = await importer.Import( - progressNotification, - new ImportTask(zipStream, string.Empty) - ); - - checkBeatmapSetCount(osu, 0); - checkBeatmapCount(osu, 0); - - Assert.IsEmpty(imported); - Assert.AreEqual(ProgressNotificationState.Cancelled, progressNotification.State); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestRollbackOnFailure() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - int itemAddRemoveFireCount = 0; - int loggedExceptionCount = 0; - - Logger.NewEntry += l => - { - if (l.Target == LoggingTarget.Database && l.Exception != null) - Interlocked.Increment(ref loggedExceptionCount); - }; - - var osu = LoadOsuIntoHost(host); - var manager = osu.Dependencies.Get(); - - // ReSharper disable once AccessToModifiedClosure - manager.ItemUpdated += _ => Interlocked.Increment(ref itemAddRemoveFireCount); - manager.ItemRemoved += _ => Interlocked.Increment(ref itemAddRemoveFireCount); - - var imported = await LoadOszIntoOsu(osu); - - Assert.AreEqual(0, itemAddRemoveFireCount -= 1); - - imported.Hash += "-changed"; - manager.Update(imported); - - Assert.AreEqual(0, itemAddRemoveFireCount -= 1); - - checkBeatmapSetCount(osu, 1); - checkBeatmapCount(osu, 12); - checkSingleReferencedFileCount(osu, 18); - - string brokenTempFilename = TestResources.GetTestBeatmapForImport(); - - MemoryStream brokenOsu = new MemoryStream(); - MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(brokenTempFilename)); - - File.Delete(brokenTempFilename); - - using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew)) - using (var zip = ZipArchive.Open(brokenOsz)) - { - zip.AddEntry("broken.osu", brokenOsu, false); - zip.SaveTo(outStream, CompressionType.Deflate); - } - - // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu. - try - { - await manager.Import(new ImportTask(brokenTempFilename)); - } - catch - { - } - - // no events should be fired in the case of a rollback. - Assert.AreEqual(0, itemAddRemoveFireCount); - - checkBeatmapSetCount(osu, 1); - checkBeatmapCount(osu, 12); - - checkSingleReferencedFileCount(osu, 18); - - Assert.AreEqual(1, loggedExceptionCount); - - File.Delete(brokenTempFilename); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportThenDeleteThenImport() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - var imported = await LoadOszIntoOsu(osu); - - deleteBeatmapSet(imported, osu); - - var importedSecondTime = await LoadOszIntoOsu(osu); - - // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. - Assert.IsTrue(imported.ID == importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - var imported = await LoadOszIntoOsu(osu); - - foreach (var b in imported.Beatmaps) - b.OnlineID = null; - - osu.Dependencies.Get().Update(imported); - - deleteBeatmapSet(imported, osu); - - var importedSecondTime = await LoadOszIntoOsu(osu); - - // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportWithDuplicateBeatmapIDs() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - var metadata = new BeatmapMetadata - { - Artist = "SomeArtist", - AuthorString = "SomeAuthor" - }; - - var difficulty = new BeatmapDifficulty(); - - var toImport = new BeatmapSetInfo - { - OnlineID = 1, - Metadata = metadata, - Beatmaps = - { - new BeatmapInfo - { - OnlineID = 2, - Metadata = metadata, - BaseDifficulty = difficulty - }, - new BeatmapInfo - { - OnlineID = 2, - Metadata = metadata, - Status = BeatmapOnlineStatus.Loved, - BaseDifficulty = difficulty - } - } - }; - - var manager = osu.Dependencies.Get(); - - var imported = await manager.Import(toImport); - - Assert.NotNull(imported); - Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineID); - Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineID); - } - finally - { - host.Exit(); - } - } - } - - [Test] - [NonParallelizable] - public void TestImportOverIPC() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(true)) - using (HeadlessGameHost client = new CleanRunHeadlessGameHost(true)) - { - try - { - Assert.IsTrue(host.IsPrimaryInstance); - Assert.IsFalse(client.IsPrimaryInstance); - - var osu = LoadOsuIntoHost(host); - - string temp = TestResources.GetTestBeatmapForImport(); - - var importer = new ArchiveImportIPCChannel(client); - if (!importer.ImportAsync(temp).Wait(10000)) - Assert.Fail(@"IPC took too long to send"); - - ensureLoaded(osu).WaitSafely(); - - waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportWhenFileOpen() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - string temp = TestResources.GetTestBeatmapForImport(); - using (File.OpenRead(temp)) - await osu.Dependencies.Get().Import(temp); - await ensureLoaded(osu); - File.Delete(temp); - Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportWithDuplicateHashes() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - string temp = TestResources.GetTestBeatmapForImport(); - - string extractedFolder = $"{temp}_extracted"; - Directory.CreateDirectory(extractedFolder); - - try - { - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(extractedFolder); - - using (var zip = ZipArchive.Create()) - { - zip.AddAllFromDirectory(extractedFolder); - zip.AddEntry("duplicate.osu", Directory.GetFiles(extractedFolder, "*.osu").First()); - zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); - } - - await osu.Dependencies.Get().Import(temp); - - await ensureLoaded(osu); - } - finally - { - Directory.Delete(extractedFolder, true); - } - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportNestedStructure() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - string temp = TestResources.GetTestBeatmapForImport(); - - string extractedFolder = $"{temp}_extracted"; - string subfolder = Path.Combine(extractedFolder, "subfolder"); - - Directory.CreateDirectory(subfolder); - - try - { - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(subfolder); - - using (var zip = ZipArchive.Create()) - { - zip.AddAllFromDirectory(extractedFolder); - zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); - } - - var imported = await osu.Dependencies.Get().Import(new ImportTask(temp)); - - await ensureLoaded(osu); - - Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); - } - finally - { - Directory.Delete(extractedFolder, true); - } - } - finally - { - host.Exit(); - } - } - } - - [Test] - public async Task TestImportWithIgnoredDirectoryInArchive() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - string temp = TestResources.GetTestBeatmapForImport(); - - string extractedFolder = $"{temp}_extracted"; - string dataFolder = Path.Combine(extractedFolder, "actual_data"); - string resourceForkFolder = Path.Combine(extractedFolder, "__MACOSX"); - string resourceForkFilePath = Path.Combine(resourceForkFolder, ".extracted"); - - Directory.CreateDirectory(dataFolder); - Directory.CreateDirectory(resourceForkFolder); - - using (var resourceForkFile = File.CreateText(resourceForkFilePath)) - { - await resourceForkFile.WriteLineAsync("adding content so that it's not empty"); - } - - try - { - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(dataFolder); - - using (var zip = ZipArchive.Create()) - { - zip.AddAllFromDirectory(extractedFolder); - zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); - } - - var imported = await osu.Dependencies.Get().Import(new ImportTask(temp)); - - await ensureLoaded(osu); - - Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored"); - Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder"); - } - finally - { - Directory.Delete(extractedFolder, true); - } - } - finally - { - host.Exit(); - } - } - } - - [Test] - public void TestUpdateBeatmapInfo() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - var manager = osu.Dependencies.Get(); - - string temp = TestResources.GetTestBeatmapForImport(); - - osu.Dependencies.Get().Import(temp).WaitSafely(); - - // Update via the beatmap, not the beatmap info, to ensure correct linking - BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0]; - Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap; - beatmapToUpdate.BeatmapInfo.DifficultyName = "updated"; - - manager.Update(setToUpdate); - - BeatmapInfo updatedInfo = manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID); - Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated")); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public void TestUpdateBeatmapFile() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - var manager = osu.Dependencies.Get(); - - string temp = TestResources.GetTestBeatmapForImport(); - - osu.Dependencies.Get().Import(temp).WaitSafely(); - - BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0]; - - var beatmapInfo = setToUpdate.Beatmaps.First(b => b.RulesetID == 0); - Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap; - BeatmapSetFileInfo fileToUpdate = setToUpdate.Files.First(f => beatmapToUpdate.BeatmapInfo.Path.Contains(f.Filename)); - - string oldMd5Hash = beatmapToUpdate.BeatmapInfo.MD5Hash; - - beatmapToUpdate.HitObjects.Clear(); - beatmapToUpdate.HitObjects.Add(new HitCircle { StartTime = 5000 }); - - manager.Save(beatmapInfo, beatmapToUpdate); - - // Check that the old file reference has been removed - Assert.That(manager.QueryBeatmapSet(s => s.ID == setToUpdate.ID).Files.All(f => f.ID != fileToUpdate.ID)); - - // Check that the new file is referenced correctly by attempting a retrieval - Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID)).Beatmap; - Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1)); - Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000)); - Assert.That(updatedBeatmap.BeatmapInfo.MD5Hash, Is.Not.EqualTo(oldMd5Hash)); - } - finally - { - host.Exit(); - } - } - } - - // TODO: needs to be pulled across to realm implementation when this file is nuked. - [Test] - public void TestSaveRemovesInvalidCharactersFromPath() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - - var manager = osu.Dependencies.Get(); - - var working = manager.CreateNew(new OsuRuleset().RulesetInfo, APIUser.SYSTEM_USER); - - var beatmap = working.Beatmap; - - beatmap.BeatmapInfo.DifficultyName = "difficulty"; - beatmap.BeatmapInfo.Metadata = new BeatmapMetadata - { - Artist = "Artist/With\\Slashes", - Title = "Title", - AuthorString = "mapper", - }; - - manager.Save(beatmap.BeatmapInfo, working.Beatmap); - - Assert.AreEqual("Artist_With_Slashes - Title (mapper) [difficulty].osu", beatmap.BeatmapInfo.Path); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public void TestCreateNewEmptyBeatmap() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - var manager = osu.Dependencies.Get(); - - var working = manager.CreateNew(new OsuRuleset().RulesetInfo, APIUser.SYSTEM_USER); - - manager.Save(working.BeatmapInfo, working.Beatmap); - - var retrievedSet = manager.GetAllUsableBeatmapSets()[0]; - - // Check that the new file is referenced correctly by attempting a retrieval - Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap; - Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(0)); - } - finally - { - host.Exit(); - } - } - } - - [Test] - public void TestCreateNewBeatmapWithObject() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host); - var manager = osu.Dependencies.Get(); - - var working = manager.CreateNew(new OsuRuleset().RulesetInfo, APIUser.SYSTEM_USER); - - ((Beatmap)working.Beatmap).HitObjects.Add(new HitCircle { StartTime = 5000 }); - - manager.Save(working.BeatmapInfo, working.Beatmap); - - var retrievedSet = manager.GetAllUsableBeatmapSets()[0]; - - // Check that the new file is referenced correctly by attempting a retrieval - Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap; - Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1)); - Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000)); - } - finally - { - host.Exit(); - } - } - } - - public static Task LoadQuickOszIntoOsu(OsuGameBase osu) => Task.Factory.StartNew(() => + public static async Task LoadQuickOszIntoOsu(OsuGameBase osu) { string temp = TestResources.GetQuickTestBeatmapForImport(); @@ -1020,16 +61,6 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } - private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo) - { - return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo - { - OnlineID = 2, - BeatmapInfo = beatmapInfo, - BeatmapInfoID = beatmapInfo.ID - }, new ImportScoreTest.TestArchiveReader()); - } - private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) { var manager = osu.Dependencies.Get(); @@ -1064,7 +95,7 @@ namespace osu.Game.Tests.Beatmaps.IO // ensure we were stored to beatmap database backing... Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1)."); - IEnumerable queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineID == 241526 && s.BaseDifficultyID > 0); + IEnumerable queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineID == 241526); IEnumerable queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineID == 241526); // if we don't re-check here, the set will be inserted but the beatmaps won't be present yet. From 0b6c4497bd0a9f74d4153a218afb603b88d9b46c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Nov 2021 18:59:14 +0900 Subject: [PATCH 003/227] Rename EF classes to allow for shit to hit the fan --- ...apDifficulty.cs => EFBeatmapDifficulty.cs} | 14 ++++---- .../{BeatmapInfo.cs => EFBeatmapInfo.cs} | 32 +++++++++---------- ...eatmapMetadata.cs => EFBeatmapMetadata.cs} | 8 ++--- ...{BeatmapSetInfo.cs => EFBeatmapSetInfo.cs} | 8 ++--- osu.Game/Database/OsuDbContext.cs | 28 ++++++++-------- .../{RulesetInfo.cs => EFRulesetInfo.cs} | 6 ++-- osu.sln.DotSettings | 2 +- 7 files changed, 49 insertions(+), 49 deletions(-) rename osu.Game/Beatmaps/{BeatmapDifficulty.cs => EFBeatmapDifficulty.cs} (81%) rename osu.Game/Beatmaps/{BeatmapInfo.cs => EFBeatmapInfo.cs} (76%) rename osu.Game/Beatmaps/{BeatmapMetadata.cs => EFBeatmapMetadata.cs} (86%) rename osu.Game/Beatmaps/{BeatmapSetInfo.cs => EFBeatmapSetInfo.cs} (90%) rename osu.Game/Rulesets/{RulesetInfo.cs => EFRulesetInfo.cs} (86%) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/EFBeatmapDifficulty.cs similarity index 81% rename from osu.Game/Beatmaps/BeatmapDifficulty.cs rename to osu.Game/Beatmaps/EFBeatmapDifficulty.cs index 65d1fb8286..843c6ac6bf 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/EFBeatmapDifficulty.cs @@ -6,7 +6,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { - public class BeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo + public class EFBeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo { /// /// The default value used for all difficulty settings except and . @@ -23,11 +23,11 @@ namespace osu.Game.Beatmaps private float? approachRate; - public BeatmapDifficulty() + public EFBeatmapDifficulty() { } - public BeatmapDifficulty(IBeatmapDifficultyInfo source) + public EFBeatmapDifficulty(IBeatmapDifficultyInfo source) { CopyFrom(source); } @@ -42,11 +42,11 @@ namespace osu.Game.Beatmaps public double SliderTickRate { get; set; } = 1; /// - /// Returns a shallow-clone of this . + /// Returns a shallow-clone of this . /// - public BeatmapDifficulty Clone() + public EFBeatmapDifficulty Clone() { - var diff = (BeatmapDifficulty)Activator.CreateInstance(GetType()); + var diff = (EFBeatmapDifficulty)Activator.CreateInstance(GetType()); CopyTo(diff); return diff; } @@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps SliderTickRate = other.SliderTickRate; } - public virtual void CopyTo(BeatmapDifficulty other) + public virtual void CopyTo(EFBeatmapDifficulty other) { other.ApproachRate = ApproachRate; other.DrainRate = DrainRate; diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/EFBeatmapInfo.cs similarity index 76% rename from osu.Game/Beatmaps/BeatmapInfo.cs rename to osu.Game/Beatmaps/EFBeatmapInfo.cs index 4175d7ff6b..2336eeb456 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/EFBeatmapInfo.cs @@ -16,7 +16,7 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] - public class BeatmapInfo : IEquatable, IHasPrimaryKey, IBeatmapInfo + public class EFBeatmapInfo : IEquatable, IHasPrimaryKey, IBeatmapInfo { public int ID { get; set; } @@ -40,14 +40,14 @@ namespace osu.Game.Beatmaps public BeatmapOnlineStatus Status { get; set; } = BeatmapOnlineStatus.None; [Required] - public BeatmapSetInfo BeatmapSet { get; set; } + public EFBeatmapSetInfo BeatmapSet { get; set; } - public BeatmapMetadata Metadata { get; set; } + public EFBeatmapMetadata Metadata { get; set; } [JsonIgnore] public int BaseDifficultyID { get; set; } - public BeatmapDifficulty BaseDifficulty { get; set; } + public EFBeatmapDifficulty BaseDifficulty { get; set; } [NotMapped] public APIBeatmap OnlineInfo { get; set; } @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps public int RulesetID { get; set; } - public RulesetInfo Ruleset { get; set; } + public EFRulesetInfo Ruleset { get; set; } public bool LetterboxInBreaks { get; set; } public bool WidescreenStoryboard { get; set; } @@ -131,7 +131,7 @@ namespace osu.Game.Beatmaps public override string ToString() => this.GetDisplayTitle(); - public bool Equals(BeatmapInfo other) + public bool Equals(EFBeatmapInfo other) { if (ReferenceEquals(this, other)) return true; if (other == null) return false; @@ -142,20 +142,20 @@ namespace osu.Game.Beatmaps return false; } - public bool Equals(IBeatmapInfo other) => other is BeatmapInfo b && Equals(b); + public bool Equals(IBeatmapInfo other) => other is EFBeatmapInfo b && Equals(b); - public bool AudioEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && - BeatmapSet.Hash == other.BeatmapSet.Hash && - (Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile; + public bool AudioEquals(EFBeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && + BeatmapSet.Hash == other.BeatmapSet.Hash && + (Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile; - public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && - BeatmapSet.Hash == other.BeatmapSet.Hash && - (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile; + public bool BackgroundEquals(EFBeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && + BeatmapSet.Hash == other.BeatmapSet.Hash && + (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile; /// - /// Returns a shallow-clone of this . + /// Returns a shallow-clone of this . /// - public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone(); + public EFBeatmapInfo Clone() => (EFBeatmapInfo)MemberwiseClone(); #region Implementation of IHasOnlineID @@ -166,7 +166,7 @@ namespace osu.Game.Beatmaps #region Implementation of IBeatmapInfo [JsonIgnore] - IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new BeatmapMetadata(); + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new EFBeatmapMetadata(); [JsonIgnore] IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty; diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/EFBeatmapMetadata.cs similarity index 86% rename from osu.Game/Beatmaps/BeatmapMetadata.cs rename to osu.Game/Beatmaps/EFBeatmapMetadata.cs index 5da0264893..6c192088dc 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/EFBeatmapMetadata.cs @@ -16,7 +16,7 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] - public class BeatmapMetadata : IEquatable, IHasPrimaryKey, IBeatmapMetadataInfo + public class EFBeatmapMetadata : IEquatable, IHasPrimaryKey, IBeatmapMetadataInfo { public int ID { get; set; } @@ -33,10 +33,10 @@ namespace osu.Game.Beatmaps public string ArtistUnicode { get; set; } = string.Empty; [JsonIgnore] - public List Beatmaps { get; set; } = new List(); + public List Beatmaps { get; set; } = new List(); [JsonIgnore] - public List BeatmapSets { get; set; } = new List(); + public List BeatmapSets { get; set; } = new List(); /// /// The author of the beatmaps in this set. @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps public string BackgroundFile { get; set; } = string.Empty; - public bool Equals(BeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other); + public bool Equals(EFBeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other); public override string ToString() => this.GetDisplayTitle(); diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/EFBeatmapSetInfo.cs similarity index 90% rename from osu.Game/Beatmaps/BeatmapSetInfo.cs rename to osu.Game/Beatmaps/EFBeatmapSetInfo.cs index a3a8f8555f..c0310f5a76 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/EFBeatmapSetInfo.cs @@ -14,7 +14,7 @@ using osu.Game.Extensions; namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] - public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable, IBeatmapSetInfo + public class EFBeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable, IBeatmapSetInfo { public int ID { get; set; } @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps public DateTimeOffset DateAdded { get; set; } - public BeatmapMetadata Metadata { get; set; } + public EFBeatmapMetadata Metadata { get; set; } [NotNull] public List Beatmaps { get; } = new List(); @@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps public bool Protected { get; set; } - public bool Equals(BeatmapSetInfo other) + public bool Equals(EFBeatmapSetInfo other) { if (ReferenceEquals(this, other)) return true; if (other == null) return false; @@ -85,7 +85,7 @@ namespace osu.Game.Beatmaps return false; } - public bool Equals(IBeatmapSetInfo other) => other is BeatmapSetInfo b && Equals(b); + public bool Equals(IBeatmapSetInfo other) => other is EFBeatmapSetInfo b && Equals(b); #region Implementation of IHasOnlineID diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 7cd9ae2885..9f0bae4a66 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -19,12 +19,12 @@ namespace osu.Game.Database { public class OsuDbContext : DbContext { - public DbSet BeatmapInfo { get; set; } - public DbSet BeatmapDifficulty { get; set; } - public DbSet BeatmapMetadata { get; set; } - public DbSet BeatmapSetInfo { get; set; } + public DbSet EFBeatmapInfo { get; set; } + public DbSet BeatmapDifficulty { get; set; } + public DbSet BeatmapMetadata { get; set; } + public DbSet EFBeatmapSetInfo { get; set; } public DbSet FileInfo { get; set; } - public DbSet RulesetInfo { get; set; } + public DbSet RulesetInfo { get; set; } public DbSet SkinInfo { get; set; } public DbSet ScoreInfo { get; set; } @@ -125,13 +125,13 @@ namespace osu.Game.Database { base.OnModelCreating(modelBuilder); - modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique(); - modelBuilder.Entity().HasIndex(b => b.MD5Hash); - modelBuilder.Entity().HasIndex(b => b.Hash); + modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.MD5Hash); + modelBuilder.Entity().HasIndex(b => b.Hash); - modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique(); - modelBuilder.Entity().HasIndex(b => b.DeletePending); - modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.DeletePending); + modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); modelBuilder.Entity().HasIndex(b => b.DeletePending); @@ -142,10 +142,10 @@ namespace osu.Game.Database modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); modelBuilder.Entity().HasIndex(b => b.ReferenceCount); - modelBuilder.Entity().HasIndex(b => b.Available); - modelBuilder.Entity().HasIndex(b => b.ShortName).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.Available); + modelBuilder.Entity().HasIndex(b => b.ShortName).IsUnique(); - modelBuilder.Entity().HasOne(b => b.BaseDifficulty); + modelBuilder.Entity().HasOne(b => b.BaseDifficulty); modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique(); } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs similarity index 86% rename from osu.Game/Rulesets/RulesetInfo.cs rename to osu.Game/Rulesets/EFRulesetInfo.cs index d018cc4194..36e2add557 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -10,7 +10,7 @@ using osu.Framework.Testing; namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] - public sealed class RulesetInfo : IEquatable, IRulesetInfo + public sealed class EFRulesetInfo : IEquatable, IRulesetInfo { public int? ID { get; set; } @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets return ruleset; } - public bool Equals(RulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; - public override bool Equals(object obj) => obj is RulesetInfo rulesetInfo && Equals(rulesetInfo); + public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo); public bool Equals(IRulesetInfo other) => other is RulesetInfo b && Equals(b); diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 44d75f265c..0da109ae7c 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -789,7 +789,7 @@ See the LICENCE file in the repository root for full licence text. <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - True + True True True True From 618903c2178b0ce4ae322de5f417938dc35f97b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Nov 2021 19:07:21 +0900 Subject: [PATCH 004/227] Rename realm to become imposter classes --- .../Database/BeatmapImporterTests.cs | 64 +++++++++---------- osu.Game.Tests/Database/GeneralUsageTests.cs | 4 +- osu.Game.Tests/Database/RealmLiveTests.cs | 46 ++++++------- osu.Game.Tests/Database/RealmTest.cs | 20 +++--- osu.Game.Tests/Database/RulesetStoreTests.cs | 6 +- .../BeatmapDifficulty.cs} | 13 ++-- .../BeatmapInfo.cs} | 26 ++++---- .../BeatmapMetadata.cs} | 6 +- .../BeatmapSetInfo.cs} | 14 ++-- osu.Game/Database/RealmContextFactory.cs | 13 ++-- .../RulesetInfo.cs} | 17 +++-- osu.Game/Stores/BeatmapImporter.cs | 40 ++++++------ osu.Game/Stores/RealmRulesetStore.cs | 11 ++-- 13 files changed, 140 insertions(+), 140 deletions(-) rename osu.Game/{Models/RealmBeatmapDifficulty.cs => Beatmaps/BeatmapDifficulty.cs} (76%) rename osu.Game/{Models/RealmBeatmap.cs => Beatmaps/BeatmapInfo.cs} (80%) rename osu.Game/{Models/RealmBeatmapMetadata.cs => Beatmaps/BeatmapMetadata.cs} (91%) rename osu.Game/{Models/RealmBeatmapSet.cs => Beatmaps/BeatmapSetInfo.cs} (84%) rename osu.Game/{Models/RealmRuleset.cs => Rulesets/RulesetInfo.cs} (74%) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index e47e24021f..37c2007681 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -19,6 +19,7 @@ using osu.Game.Extensions; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; using osu.Game.Stores; using osu.Game.Tests.Resources; using Realms; @@ -42,25 +43,25 @@ namespace osu.Game.Tests.Database using (var importer = new BeatmapImporter(realmFactory, storage)) using (new RealmRulesetStore(realmFactory, storage)) { - ILive? imported; + ILive? imported; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) imported = await importer.Import(reader); - Assert.AreEqual(1, realmFactory.Context.All().Count()); + Assert.AreEqual(1, realmFactory.Context.All().Count()); Assert.NotNull(imported); Debug.Assert(imported != null); imported.PerformWrite(s => s.DeletePending = true); - Assert.AreEqual(1, realmFactory.Context.All().Count(s => s.DeletePending)); + Assert.AreEqual(1, realmFactory.Context.All().Count(s => s.DeletePending)); } }); Logger.Log("Running with no work to purge pending deletions"); - RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All().Count()); }); + RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All().Count()); }); } [Test] @@ -117,7 +118,7 @@ namespace osu.Game.Tests.Database string? tempPath = TestResources.GetTestBeatmapForImport(); - ILive? importedSet; + ILive? importedSet; using (var stream = File.OpenRead(tempPath)) { @@ -131,7 +132,7 @@ namespace osu.Game.Tests.Database Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); File.Delete(tempPath); - var imported = realmFactory.Context.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); + var imported = realmFactory.Context.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); deleteBeatmapSet(imported, realmFactory.Context); }); @@ -556,7 +557,7 @@ namespace osu.Game.Tests.Database using var importer = new BeatmapImporter(realmFactory, storage); using var store = new RealmRulesetStore(realmFactory, storage); - var metadata = new RealmBeatmapMetadata + var metadata = new BeatmapMetadata { Artist = "SomeArtist", Author = @@ -565,18 +566,18 @@ namespace osu.Game.Tests.Database } }; - var ruleset = realmFactory.Context.All().First(); + var ruleset = realmFactory.Context.All().First(); - var toImport = new RealmBeatmapSet + var toImport = new BeatmapSetInfo { OnlineID = 1, Beatmaps = { - new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) + new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { OnlineID = 2, }, - new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) + new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { OnlineID = 2, Status = BeatmapOnlineStatus.Loved, @@ -752,18 +753,18 @@ namespace osu.Game.Tests.Database await importer.Import(temp); // Update via the beatmap, not the beatmap info, to ensure correct linking - RealmBeatmapSet setToUpdate = realmFactory.Context.All().First(); + BeatmapSetInfo setToUpdate = realmFactory.Context.All().First(); var beatmapToUpdate = setToUpdate.Beatmaps.First(); realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated"); - RealmBeatmap updatedInfo = realmFactory.Context.All().First(b => b.ID == beatmapToUpdate.ID); + BeatmapInfo updatedInfo = realmFactory.Context.All().First(b => b.ID == beatmapToUpdate.ID); Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated")); }); } - public static async Task LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm) + public static async Task LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm) { string? temp = TestResources.GetQuickTestBeatmapForImport(); @@ -775,10 +776,10 @@ namespace osu.Game.Tests.Database waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return realm.All().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID); + return realm.All().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID); } - public static async Task LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false) + public static async Task LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false) { string? temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); @@ -791,20 +792,20 @@ namespace osu.Game.Tests.Database waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return realm.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); + return realm.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); } - private void deleteBeatmapSet(RealmBeatmapSet imported, Realm realm) + private void deleteBeatmapSet(BeatmapSetInfo imported, Realm realm) { realm.Write(() => imported.DeletePending = true); checkBeatmapSetCount(realm, 0); checkBeatmapSetCount(realm, 1, true); - Assert.IsTrue(realm.All().First(_ => true).DeletePending); + Assert.IsTrue(realm.All().First(_ => true).DeletePending); } - private static Task createScoreForBeatmap(Realm realm, RealmBeatmap beatmap) + private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) { // TODO: reimplement when we have score support in realm. // return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo @@ -820,8 +821,8 @@ namespace osu.Game.Tests.Database private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false) { Assert.AreEqual(expected, includeDeletePending - ? realm.All().Count() - : realm.All().Count(s => !s.DeletePending)); + ? realm.All().Count() + : realm.All().Count(s => !s.DeletePending)); } private static string hashFile(string filename) @@ -832,7 +833,7 @@ namespace osu.Game.Tests.Database private static void checkBeatmapCount(Realm realm, int expected) { - Assert.AreEqual(expected, realm.All().Where(_ => true).ToList().Count); + Assert.AreEqual(expected, realm.All().Where(_ => true).ToList().Count); } private static void checkSingleReferencedFileCount(Realm realm, int expected) @@ -850,24 +851,23 @@ namespace osu.Game.Tests.Database private static void ensureLoaded(Realm realm, int timeout = 60000) { - IQueryable? resultSets = null; + IQueryable? resultSets = null; waitForOrAssert(() => - { - realm.Refresh(); - return (resultSets = realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(); - }, - @"BeatmapSet did not import to the database in allocated time.", timeout); + { + realm.Refresh(); + return (resultSets = realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(); + }, @"BeatmapSet did not import to the database in allocated time.", timeout); // ensure we were stored to beatmap database backing... Assert.IsTrue(resultSets?.Count() == 1, $@"Incorrect result count found ({resultSets?.Count()} but should be 1)."); - IEnumerable queryBeatmapSets() => realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526); + IEnumerable queryBeatmapSets() => realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526); var set = queryBeatmapSets().First(); // ReSharper disable once PossibleUnintendedReferenceComparison - IEnumerable queryBeatmaps() => realm.All().Where(s => s.BeatmapSet != null && s.BeatmapSet == set); + IEnumerable queryBeatmaps() => realm.All().Where(s => s.BeatmapSet != null && s.BeatmapSet == set); Assert.AreEqual(12, queryBeatmaps().Count(), @"Beatmap count was not correct"); Assert.AreEqual(1, queryBeatmapSets().Count(), @"Beatmapset count was not correct"); @@ -880,7 +880,7 @@ namespace osu.Game.Tests.Database countBeatmaps = queryBeatmaps().Count(), $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps})."); - foreach (RealmBeatmap b in set.Beatmaps) + foreach (BeatmapInfo b in set.Beatmaps) Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID)); Assert.IsTrue(set.Beatmaps.Count > 0); } diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 2285b22a3a..0961ad71e4 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -5,8 +5,8 @@ using System; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Models; #nullable enable @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Database using (var context = realmFactory.CreateContext()) { - var subscription = context.All().QueryAsyncWithNotifications((sender, changes, error) => + var subscription = context.All().QueryAsyncWithNotifications((sender, changes, error) => { using (realmFactory.CreateContext()) { diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 9432a56741..187fcd3ca7 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -8,8 +8,8 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Models; using Realms; #nullable enable @@ -23,9 +23,9 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(realmFactory); + ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory); - ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(realmFactory); + ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(realmFactory); Assert.AreEqual(beatmap, beatmap2); }); @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, storage) => { - var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); - ILive liveBeatmap; + ILive liveBeatmap; using (var context = realmFactory.CreateContext()) { @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); var liveBeatmap = beatmap.ToLive(realmFactory); @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Database [Test] public void TestAccessNonManaged() { - var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); var liveBeatmap = beatmap.ToLiveUnmanaged(); Assert.IsFalse(beatmap.Hidden); @@ -96,12 +96,12 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive? liveBeatmap = null; + ILive? liveBeatmap = null; Task.Factory.StartNew(() => { using (var threadContext = realmFactory.CreateContext()) { - var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); } @@ -125,12 +125,12 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive? liveBeatmap = null; + ILive? liveBeatmap = null; Task.Factory.StartNew(() => { using (var threadContext = realmFactory.CreateContext()) { - var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); } @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); var liveBeatmap = beatmap.ToLive(realmFactory); Assert.DoesNotThrow(() => @@ -166,13 +166,13 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive? liveBeatmap = null; + ILive? liveBeatmap = null; Task.Factory.StartNew(() => { using (var threadContext = realmFactory.CreateContext()) { - var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); } @@ -205,12 +205,12 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive? liveBeatmap = null; + ILive? liveBeatmap = null; Task.Factory.StartNew(() => { using (var threadContext = realmFactory.CreateContext()) { - var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); } @@ -237,19 +237,19 @@ namespace osu.Game.Tests.Database using (var updateThreadContext = realmFactory.CreateContext()) { - updateThreadContext.All().QueryAsyncWithNotifications(gotChange); - ILive? liveBeatmap = null; + updateThreadContext.All().QueryAsyncWithNotifications(gotChange); + ILive? liveBeatmap = null; Task.Factory.StartNew(() => { using (var threadContext = realmFactory.CreateContext()) { var ruleset = CreateRuleset(); - var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); // add a second beatmap to ensure that a full refresh occurs below. // not just a refresh from the resolved Live. - threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); liveBeatmap = beatmap.ToLive(realmFactory); } @@ -258,14 +258,14 @@ namespace osu.Game.Tests.Database Debug.Assert(liveBeatmap != null); // not yet seen by main context - Assert.AreEqual(0, updateThreadContext.All().Count()); + Assert.AreEqual(0, updateThreadContext.All().Count()); Assert.AreEqual(0, changesTriggered); liveBeatmap.PerformRead(resolved => { // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. // ReSharper disable once AccessToDisposedClosure - Assert.AreEqual(2, updateThreadContext.All().Count()); + Assert.AreEqual(2, updateThreadContext.All().Count()); Assert.AreEqual(1, changesTriggered); // can access properties without a crash. @@ -280,7 +280,7 @@ namespace osu.Game.Tests.Database }); } - void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) + void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) { changesTriggered++; } diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 4e67f09dca..0cee165f75 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -9,9 +9,11 @@ using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO; using osu.Game.Models; +using osu.Game.Rulesets; #nullable enable @@ -74,24 +76,24 @@ namespace osu.Game.Tests.Database } } - protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset) + protected static BeatmapSetInfo CreateBeatmapSet(RulesetInfo ruleset) { RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() }; - var metadata = new RealmBeatmapMetadata + var metadata = new BeatmapMetadata { Title = "My Love", Artist = "Kuba Oms" }; - var beatmapSet = new RealmBeatmapSet + var beatmapSet = new BeatmapSetInfo { Beatmaps = { - new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", }, - new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", }, - new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", }, - new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", } + new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { DifficultyName = "Easy", }, + new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { DifficultyName = "Normal", }, + new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { DifficultyName = "Hard", }, + new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata) { DifficultyName = "Insane", } }, Files = { @@ -111,8 +113,8 @@ namespace osu.Game.Tests.Database return beatmapSet; } - protected static RealmRuleset CreateRuleset() => - new RealmRuleset(0, "osu!", "osu", true); + protected static RulesetInfo CreateRuleset() => + new RulesetInfo(0, "osu!", "osu", true); private class RealmTestGame : Framework.Game { diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index cc7e8a0c97..d2de31d11a 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -3,7 +3,7 @@ using System.Linq; using NUnit.Framework; -using osu.Game.Models; +using osu.Game.Rulesets; using osu.Game.Stores; namespace osu.Game.Tests.Database @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Database var rulesets = new RealmRulesetStore(realmFactory, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); - Assert.AreEqual(4, realmFactory.Context.All().Count()); + Assert.AreEqual(4, realmFactory.Context.All().Count()); }); } @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Database Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First()); - Assert.AreEqual(4, realmFactory.Context.All().Count()); + Assert.AreEqual(4, realmFactory.Context.All().Count()); }); } diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs similarity index 76% rename from osu.Game/Models/RealmBeatmapDifficulty.cs rename to osu.Game/Beatmaps/BeatmapDifficulty.cs index 3c1dad69e4..6425c7b291 100644 --- a/osu.Game/Models/RealmBeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -2,16 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Testing; -using osu.Game.Beatmaps; using Realms; #nullable enable -namespace osu.Game.Models +namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [MapTo("BeatmapDifficulty")] - public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo + public class BeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo { public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; @@ -22,16 +21,16 @@ namespace osu.Game.Models public double SliderTickRate { get; set; } = 1; /// - /// Returns a shallow-clone of this . + /// Returns a shallow-clone of this . /// - public RealmBeatmapDifficulty Clone() + public BeatmapDifficulty Clone() { - var diff = new RealmBeatmapDifficulty(); + var diff = new BeatmapDifficulty(); CopyTo(diff); return diff; } - public void CopyTo(RealmBeatmapDifficulty difficulty) + public void CopyTo(BeatmapDifficulty difficulty) { difficulty.ApproachRate = ApproachRate; difficulty.DrainRate = DrainRate; diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Beatmaps/BeatmapInfo.cs similarity index 80% rename from osu.Game/Models/RealmBeatmap.cs rename to osu.Game/Beatmaps/BeatmapInfo.cs index 8e132687f7..b000862457 100644 --- a/osu.Game/Models/RealmBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -6,14 +6,14 @@ using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Testing; -using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Models; using osu.Game.Rulesets; using Realms; #nullable enable -namespace osu.Game.Models +namespace osu.Game.Beatmaps { /// /// A single beatmap difficulty. @@ -21,20 +21,20 @@ namespace osu.Game.Models [ExcludeFromDynamicCompile] [Serializable] [MapTo("Beatmap")] - public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo, IEquatable + public class BeatmapInfo : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo, IEquatable { [PrimaryKey] public Guid ID { get; set; } = Guid.NewGuid(); public string DifficultyName { get; set; } = string.Empty; - public RealmRuleset Ruleset { get; set; } = null!; + public RulesetInfo Ruleset { get; set; } = null!; - public RealmBeatmapDifficulty Difficulty { get; set; } = null!; + public BeatmapDifficulty Difficulty { get; set; } = null!; - public RealmBeatmapMetadata Metadata { get; set; } = null!; + public BeatmapMetadata Metadata { get; set; } = null!; - public RealmBeatmapSet? BeatmapSet { get; set; } + public BeatmapSetInfo? BeatmapSet { get; set; } [Ignored] public RealmNamedFileUsage? File => BeatmapSet?.Files.First(f => f.File.Hash == Hash); @@ -64,7 +64,7 @@ namespace osu.Game.Models [JsonIgnore] public bool Hidden { get; set; } - public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata) + public BeatmapInfo(RulesetInfo ruleset, BeatmapDifficulty difficulty, BeatmapMetadata metadata) { Ruleset = ruleset; Difficulty = difficulty; @@ -72,7 +72,7 @@ namespace osu.Game.Models } [UsedImplicitly] - private RealmBeatmap() + private BeatmapInfo() { } @@ -102,7 +102,7 @@ namespace osu.Game.Models #endregion - public bool Equals(RealmBeatmap? other) + public bool Equals(BeatmapInfo? other) { if (ReferenceEquals(this, other)) return true; if (other == null) return false; @@ -110,15 +110,15 @@ namespace osu.Game.Models return ID == other.ID; } - public bool Equals(IBeatmapInfo? other) => other is RealmBeatmap b && Equals(b); + public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b); - public bool AudioEquals(RealmBeatmap? other) => other != null + public bool AudioEquals(BeatmapInfo? other) => other != null && BeatmapSet != null && other.BeatmapSet != null && BeatmapSet.Hash == other.BeatmapSet.Hash && Metadata.AudioFile == other.Metadata.AudioFile; - public bool BackgroundEquals(RealmBeatmap? other) => other != null + public bool BackgroundEquals(BeatmapInfo? other) => other != null && BeatmapSet != null && other.BeatmapSet != null && BeatmapSet.Hash == other.BeatmapSet.Hash diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs similarity index 91% rename from osu.Game/Models/RealmBeatmapMetadata.cs rename to osu.Game/Beatmaps/BeatmapMetadata.cs index db1b09e6ad..387641db40 100644 --- a/osu.Game/Models/RealmBeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -4,18 +4,18 @@ using System; using Newtonsoft.Json; using osu.Framework.Testing; -using osu.Game.Beatmaps; +using osu.Game.Models; using osu.Game.Users; using Realms; #nullable enable -namespace osu.Game.Models +namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] [MapTo("BeatmapMetadata")] - public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo + public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo { public string Title { get; set; } = string.Empty; diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs similarity index 84% rename from osu.Game/Models/RealmBeatmapSet.cs rename to osu.Game/Beatmaps/BeatmapSetInfo.cs index 3566ff5321..4aea0c6ce8 100644 --- a/osu.Game/Models/RealmBeatmapSet.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -5,18 +5,18 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Testing; -using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; +using osu.Game.Models; using Realms; #nullable enable -namespace osu.Game.Models +namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [MapTo("BeatmapSet")] - public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo + public class BeatmapSetInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo { [PrimaryKey] public Guid ID { get; set; } = Guid.NewGuid(); @@ -26,9 +26,9 @@ namespace osu.Game.Models public DateTimeOffset DateAdded { get; set; } - public IBeatmapMetadataInfo Metadata => Beatmaps.FirstOrDefault()?.Metadata ?? new RealmBeatmapMetadata(); + public IBeatmapMetadataInfo Metadata => Beatmaps.FirstOrDefault()?.Metadata ?? new BeatmapMetadata(); - public IList Beatmaps { get; } = null!; + public IList Beatmaps { get; } = null!; public IList Files { get; } = null!; @@ -63,7 +63,7 @@ namespace osu.Game.Models /// The name of the file to get the storage path of. public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); - public bool Equals(RealmBeatmapSet? other) + public bool Equals(BeatmapSetInfo? other) { if (ReferenceEquals(this, other)) return true; if (other == null) return false; @@ -73,7 +73,7 @@ namespace osu.Game.Models public override string ToString() => Metadata.GetDisplayString(); - public bool Equals(IBeatmapSetInfo? other) => other is RealmBeatmapSet b && Equals(b); + public bool Equals(IBeatmapSetInfo? other) => other is BeatmapSetInfo b && Equals(b); IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; IEnumerable IHasNamedFiles.Files => Files; diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 96c24837a1..946dda1f39 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -12,6 +12,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Configuration; +using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Models; using osu.Game.Skinning; @@ -109,7 +110,7 @@ namespace osu.Game.Database using (var realm = CreateContext()) using (var transaction = realm.BeginWrite()) { - var pendingDeleteSets = realm.All().Where(s => s.DeletePending); + var pendingDeleteSets = realm.All().Where(s => s.DeletePending); foreach (var s in pendingDeleteSets) { @@ -206,9 +207,9 @@ namespace osu.Game.Database switch (targetVersion) { case 7: - convertOnlineIDs(); - convertOnlineIDs(); - convertOnlineIDs(); + convertOnlineIDs(); + convertOnlineIDs(); + convertOnlineIDs(); void convertOnlineIDs() where T : RealmObject { @@ -253,14 +254,14 @@ namespace osu.Game.Database case 9: // Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well. - string metadataClassName = getMappedOrOriginalName(typeof(RealmBeatmapMetadata)); + string metadataClassName = getMappedOrOriginalName(typeof(BeatmapMetadata)); // May be coming from a version before `RealmBeatmapMetadata` existed. if (!migration.OldRealm.Schema.TryFindObjectSchema(metadataClassName, out _)) return; var oldMetadata = migration.OldRealm.DynamicApi.All(metadataClassName); - var newMetadata = migration.NewRealm.All(); + var newMetadata = migration.NewRealm.All(); int metadataCount = newMetadata.Count(); diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Rulesets/RulesetInfo.cs similarity index 74% rename from osu.Game/Models/RealmRuleset.cs rename to osu.Game/Rulesets/RulesetInfo.cs index b959d0b4dc..91a3d181f8 100644 --- a/osu.Game/Models/RealmRuleset.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -4,16 +4,15 @@ using System; using JetBrains.Annotations; using osu.Framework.Testing; -using osu.Game.Rulesets; using Realms; #nullable enable -namespace osu.Game.Models +namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] [MapTo("Ruleset")] - public class RealmRuleset : RealmObject, IEquatable, IRulesetInfo + public class RulesetInfo : RealmObject, IEquatable, IRulesetInfo { [PrimaryKey] public string ShortName { get; set; } = string.Empty; @@ -25,7 +24,7 @@ namespace osu.Game.Models public string InstantiationInfo { get; set; } = string.Empty; - public RealmRuleset(string shortName, string name, string instantiationInfo, int onlineID) + public RulesetInfo(string shortName, string name, string instantiationInfo, int onlineID) { ShortName = shortName; Name = name; @@ -34,11 +33,11 @@ namespace osu.Game.Models } [UsedImplicitly] - private RealmRuleset() + private RulesetInfo() { } - public RealmRuleset(int? onlineID, string name, string shortName, bool available) + public RulesetInfo(int? onlineID, string name, string shortName, bool available) { OnlineID = onlineID ?? -1; Name = name; @@ -48,13 +47,13 @@ namespace osu.Game.Models public bool Available { get; set; } - public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + public bool Equals(RulesetInfo? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; - public bool Equals(IRulesetInfo? other) => other is RealmRuleset b && Equals(b); + public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); public override string ToString() => Name; - public RealmRuleset Clone() => new RealmRuleset + public RulesetInfo Clone() => new RulesetInfo { OnlineID = OnlineID, Name = Name, diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 8ab6941885..69c136b51c 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -34,7 +34,7 @@ namespace osu.Game.Stores /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapImporter : RealmArchiveModelImporter, IDisposable + public class BeatmapImporter : RealmArchiveModelImporter, IDisposable { public override IEnumerable HandledExtensions => new[] { ".osz" }; @@ -53,12 +53,12 @@ namespace osu.Game.Stores protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz"; - protected override Task Populate(RealmBeatmapSet beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { if (archive != null) beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm)); - foreach (RealmBeatmap b in beatmapSet.Beatmaps) + foreach (BeatmapInfo b in beatmapSet.Beatmaps) b.BeatmapSet = beatmapSet; validateOnlineIds(beatmapSet, realm); @@ -84,7 +84,7 @@ namespace osu.Game.Stores return Task.CompletedTask; } - protected override void PreImport(RealmBeatmapSet beatmapSet, Realm realm) + protected override void PreImport(BeatmapSetInfo beatmapSet, Realm realm) { // We are about to import a new beatmap. Before doing so, ensure that no other set shares the online IDs used by the new one. // Note that this means if the previous beatmap is restored by the user, it will no longer be linked to its online IDs. @@ -93,7 +93,7 @@ namespace osu.Game.Stores if (beatmapSet.OnlineID > 0) { - var existingSetWithSameOnlineID = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); + var existingSetWithSameOnlineID = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); if (existingSetWithSameOnlineID != null) { @@ -108,7 +108,7 @@ namespace osu.Game.Stores } } - private void validateOnlineIds(RealmBeatmapSet beatmapSet, Realm realm) + private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm) { var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID > 0).Select(b => b.OnlineID).ToList(); @@ -121,10 +121,10 @@ namespace osu.Game.Stores } // find any existing beatmaps in the database that have matching online ids - List existingBeatmaps = new List(); + List existingBeatmaps = new List(); foreach (int id in beatmapIds) - existingBeatmaps.AddRange(realm.All().Where(b => b.OnlineID == id)); + existingBeatmaps.AddRange(realm.All().Where(b => b.OnlineID == id)); if (existingBeatmaps.Any()) { @@ -143,7 +143,7 @@ namespace osu.Game.Stores void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = -1); } - protected override bool CanSkipImport(RealmBeatmapSet existing, RealmBeatmapSet import) + protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) { if (!base.CanSkipImport(existing, import)) return false; @@ -151,7 +151,7 @@ namespace osu.Game.Stores return existing.Beatmaps.Any(b => b.OnlineID > 0); } - protected override bool CanReuseExisting(RealmBeatmapSet existing, RealmBeatmapSet import) + protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) { if (!base.CanReuseExisting(existing, import)) return false; @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override string HumanisedModelName => "beatmap"; - protected override RealmBeatmapSet? CreateModel(ArchiveReader reader) + protected override BeatmapSetInfo? CreateModel(ArchiveReader reader) { // let's make sure there are actually .osu files to import. string? mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); @@ -180,7 +180,7 @@ namespace osu.Game.Stores using (var stream = new LineBufferedReader(reader.GetStream(mapName))) beatmap = Decoder.GetDecoder(stream).Decode(stream); - return new RealmBeatmapSet + return new BeatmapSetInfo { OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1, // Metadata = beatmap.Metadata, @@ -189,11 +189,11 @@ namespace osu.Game.Stores } /// - /// Create all required s for the provided archive. + /// Create all required s for the provided archive. /// - private List createBeatmapDifficulties(IList files, Realm realm) + private List createBeatmapDifficulties(IList files, Realm realm) { - var beatmaps = new List(); + var beatmaps = new List(); foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) { @@ -214,7 +214,7 @@ namespace osu.Game.Stores var decodedInfo = decoded.BeatmapInfo; var decodedDifficulty = decodedInfo.BaseDifficulty; - var ruleset = realm.All().FirstOrDefault(r => r.OnlineID == decodedInfo.RulesetID); + var ruleset = realm.All().FirstOrDefault(r => r.OnlineID == decodedInfo.RulesetID); if (ruleset?.Available != true) { @@ -222,7 +222,7 @@ namespace osu.Game.Stores continue; } - var difficulty = new RealmBeatmapDifficulty + var difficulty = new BeatmapDifficulty { DrainRate = decodedDifficulty.DrainRate, CircleSize = decodedDifficulty.CircleSize, @@ -232,7 +232,7 @@ namespace osu.Game.Stores SliderTickRate = decodedDifficulty.SliderTickRate, }; - var metadata = new RealmBeatmapMetadata + var metadata = new BeatmapMetadata { Title = decoded.Metadata.Title, TitleUnicode = decoded.Metadata.TitleUnicode, @@ -250,7 +250,7 @@ namespace osu.Game.Stores BackgroundFile = decoded.Metadata.BackgroundFile, }; - var beatmap = new RealmBeatmap(ruleset, difficulty, metadata) + var beatmap = new BeatmapInfo(ruleset, difficulty, metadata) { Hash = hash, DifficultyName = decodedInfo.DifficultyName, @@ -278,7 +278,7 @@ namespace osu.Game.Stores return beatmaps; } - private void updateBeatmapStatistics(RealmBeatmap beatmap, IBeatmap decoded) + private void updateBeatmapStatistics(BeatmapInfo beatmap, IBeatmap decoded) { var rulesetInstance = ((IRulesetInfo)beatmap.Ruleset).CreateInstance(); diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs index 93b6d29e7d..f208b392cb 100644 --- a/osu.Game/Stores/RealmRulesetStore.cs +++ b/osu.Game/Stores/RealmRulesetStore.cs @@ -11,7 +11,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; -using osu.Game.Models; using osu.Game.Rulesets; #nullable enable @@ -106,7 +105,7 @@ namespace osu.Game.Stores { context.Write(realm => { - var rulesets = realm.All(); + var rulesets = realm.All(); List instances = loadedAssemblies.Values .Select(r => Activator.CreateInstance(r) as Ruleset) @@ -117,8 +116,8 @@ namespace osu.Game.Stores // add all legacy rulesets first to ensure they have exclusive choice of primary key. foreach (var r in instances.Where(r => r is ILegacyRuleset)) { - if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null) - realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null) + realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); } // add any other rulesets which have assemblies present but are not yet in the database. @@ -136,11 +135,11 @@ namespace osu.Game.Stores existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; } else - realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); } } - List detachedRulesets = new List(); + List detachedRulesets = new List(); // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. foreach (var r in rulesets) From c3df58e01c741c9ed204b66fe666b6c87855b1e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Nov 2021 19:24:07 +0900 Subject: [PATCH 005/227] Add required properties to make realm models backwards compatible --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 27 +++++++++++- osu.Game/Beatmaps/BeatmapInfo.cs | 59 ++++++++++++++++++++++---- osu.Game/Beatmaps/BeatmapMetadata.cs | 10 +++++ osu.Game/Models/RealmNamedFileUsage.cs | 10 +++++ osu.Game/Rulesets/RulesetInfo.cs | 8 +++- 5 files changed, 103 insertions(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 6425c7b291..90a60c0f0c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -12,6 +12,11 @@ namespace osu.Game.Beatmaps [MapTo("BeatmapDifficulty")] public class BeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo { + /// + /// The default value used for all difficulty settings except and . + /// + public const float DEFAULT_DIFFICULTY = 5; + public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; @@ -20,6 +25,15 @@ namespace osu.Game.Beatmaps public double SliderMultiplier { get; set; } = 1; public double SliderTickRate { get; set; } = 1; + public BeatmapDifficulty() + { + } + + public BeatmapDifficulty(IBeatmapDifficultyInfo source) + { + CopyFrom(source); + } + /// /// Returns a shallow-clone of this . /// @@ -30,7 +44,7 @@ namespace osu.Game.Beatmaps return diff; } - public void CopyTo(BeatmapDifficulty difficulty) + public virtual void CopyTo(BeatmapDifficulty difficulty) { difficulty.ApproachRate = ApproachRate; difficulty.DrainRate = DrainRate; @@ -40,5 +54,16 @@ namespace osu.Game.Beatmaps difficulty.SliderMultiplier = SliderMultiplier; difficulty.SliderTickRate = SliderTickRate; } + + public virtual void CopyFrom(IBeatmapDifficultyInfo other) + { + ApproachRate = other.ApproachRate; + DrainRate = other.DrainRate; + CircleSize = other.CircleSize; + OverallDifficulty = other.OverallDifficulty; + + SliderMultiplier = other.SliderMultiplier; + SliderTickRate = other.SliderTickRate; + } } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index b000862457..796c2b2e90 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Models; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using Realms; @@ -72,7 +73,7 @@ namespace osu.Game.Beatmaps } [UsedImplicitly] - private BeatmapInfo() + public BeatmapInfo() { } @@ -100,6 +101,13 @@ namespace osu.Game.Beatmaps public double TimelineZoom { get; set; } + public CountdownType Countdown { get; set; } = CountdownType.Normal; + + /// + /// The number of beats to move the countdown backwards (compared to its default location). + /// + public int CountdownOffset { get; set; } + #endregion public bool Equals(BeatmapInfo? other) @@ -113,20 +121,53 @@ namespace osu.Game.Beatmaps public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b); public bool AudioEquals(BeatmapInfo? other) => other != null - && BeatmapSet != null - && other.BeatmapSet != null - && BeatmapSet.Hash == other.BeatmapSet.Hash - && Metadata.AudioFile == other.Metadata.AudioFile; + && BeatmapSet != null + && other.BeatmapSet != null + && BeatmapSet.Hash == other.BeatmapSet.Hash + && Metadata.AudioFile == other.Metadata.AudioFile; public bool BackgroundEquals(BeatmapInfo? other) => other != null - && BeatmapSet != null - && other.BeatmapSet != null - && BeatmapSet.Hash == other.BeatmapSet.Hash - && Metadata.BackgroundFile == other.Metadata.BackgroundFile; + && BeatmapSet != null + && other.BeatmapSet != null + && BeatmapSet.Hash == other.BeatmapSet.Hash + && Metadata.BackgroundFile == other.Metadata.BackgroundFile; IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty; + + #region Compatibility properties + + [Ignored] + public int RulesetID => Ruleset.OnlineID; + + [Ignored] + public Guid BeatmapSetInfoID => BeatmapSet?.ID ?? Guid.Empty; + + [Ignored] + public BeatmapDifficulty BaseDifficulty + { + get => Difficulty; + set => Difficulty = value; + } + + [Ignored] + public string? Path => File?.Filename; + + [Ignored] + public APIBeatmap? OnlineInfo { get; set; } + + [Ignored] + public int? MaxCombo { get; set; } + + [Ignored] + public int[] Bookmarks { get; set; } = Array.Empty(); + + public int BeatmapVersion; + + public BeatmapInfo Clone() => this.Detach(); + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 387641db40..dc5517375d 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -44,5 +44,15 @@ namespace osu.Game.Beatmaps public string BackgroundFile { get; set; } = string.Empty; IUser IBeatmapMetadataInfo.Author => Author; + + #region Compatibility properties + + public string AuthorString + { + get => Author.Username; + set => Author.Username = value; + } + + #endregion } } diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs index 17e32510a8..801c826292 100644 --- a/osu.Game/Models/RealmNamedFileUsage.cs +++ b/osu.Game/Models/RealmNamedFileUsage.cs @@ -31,5 +31,15 @@ namespace osu.Game.Models } IFileInfo INamedFileUsage.File => File; + + #region Compatibility properties + + public RealmFile FileInfo + { + get => File; + set => File = value; + } + + #endregion } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 91a3d181f8..99b4f5915f 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets } [UsedImplicitly] - private RulesetInfo() + public RulesetInfo() { } @@ -83,5 +83,11 @@ namespace osu.Game.Rulesets return ruleset; } + + #region Compatibility properties + + public int ID => OnlineID; + + #endregion } } From 4f66e8f88121cffadeab4e5df618cbb3642b35b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 14:24:17 +0900 Subject: [PATCH 006/227] Fix issues with editor check tests --- .../Editing/Checks/CheckAudioInVideoTest.cs | 2 +- .../Editing/Checks/CheckFilePresenceTest.cs | 2 +- osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs | 11 +++-------- .../Editing/Checks/CheckTooShortAudioFilesTest.cs | 3 +++ 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs index f9b7bfa586..614b9b4ac1 100644 --- a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestMissingFile() { - beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); + beatmap.BeatmapInfo.BeatmapSet?.Files.Clear(); var issues = check.Run(getContext(null)).ToList(); diff --git a/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs b/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs index f36454aa71..01baaadc7d 100644 --- a/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckFilePresenceTest.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestBackgroundSetAndNotInFiles() { - beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); + beatmap.BeatmapInfo.BeatmapSet?.Files.Clear(); var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); var issues = check.Run(context).ToList(); diff --git a/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs b/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs index f702921986..9067714ff9 100644 --- a/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs +++ b/osu.Game.Tests/Editing/Checks/CheckTestHelpers.cs @@ -1,18 +1,13 @@ // 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; +using osu.Game.Models; 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" } - }; + public static RealmNamedFileUsage CreateMockFile(string extension) => + new RealmNamedFileUsage(new RealmFile { Hash = "abcdef" }, $"abc123.{extension}"); } } diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs index 8adf0d3764..242fec2f68 100644 --- a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs @@ -1,6 +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.Diagnostics; using System.IO; using System.Linq; using ManagedBass; @@ -45,6 +46,8 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestDifferentExtension() { + Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); + beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); beatmap.BeatmapInfo.BeatmapSet.Files.Add(CheckTestHelpers.CreateMockFile("jpg")); From e6f6558ddff8b02a4cbb002bce38259993de73a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 14:26:51 +0900 Subject: [PATCH 007/227] Update mock model usage to set `GUID`s instead of `int`s --- .../TestSceneBeatmapDifficultyCache.cs | 18 ++++++++++-------- .../NonVisual/BeatmapSetInfoEqualityTest.cs | 9 ++++++--- .../SongSelect/TestSceneBeatmapCarousel.cs | 2 +- .../SongSelect/TestScenePlaySongSelect.cs | 2 +- .../UserInterface/TestSceneDeleteLocalScore.cs | 4 ++-- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs index 26ab8808b9..298e474fd1 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs @@ -24,6 +24,8 @@ namespace osu.Game.Tests.Beatmaps { public const double BASE_STARS = 5.55; + private static readonly Guid guid = Guid.NewGuid(); + private BeatmapSetInfo importedSet; private TestBeatmapDifficultyCache difficultyCache; @@ -98,8 +100,8 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestKeyEqualsWithDifferentModInstances() { - var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); Assert.That(key1, Is.EqualTo(key2)); Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode())); @@ -108,8 +110,8 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestKeyEqualsWithDifferentModOrder() { - var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); Assert.That(key1, Is.EqualTo(key2)); Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode())); @@ -118,8 +120,8 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestKeyDoesntEqualWithDifferentModSettings() { - var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } }); - var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } }); + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } }); Assert.That(key1, Is.Not.EqualTo(key2)); Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode())); @@ -128,8 +130,8 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestKeyEqualWithMatchingModSettings() { - var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); - var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); Assert.That(key1, Is.EqualTo(key2)); Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode())); diff --git a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs index 534983f869..1b6049fcb7 100644 --- a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs +++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs @@ -1,6 +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; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -23,8 +24,10 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestDatabasedWithDatabased() { - var ourInfo = new BeatmapSetInfo { ID = 123 }; - var otherInfo = new BeatmapSetInfo { ID = 123 }; + var guid = Guid.NewGuid(); + + var ourInfo = new BeatmapSetInfo { ID = guid }; + var otherInfo = new BeatmapSetInfo { ID = guid }; Assert.AreEqual(ourInfo, otherInfo); } @@ -32,7 +35,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestDatabasedWithOnline() { - var ourInfo = new BeatmapSetInfo { ID = 123, OnlineID = 12 }; + var ourInfo = new BeatmapSetInfo { ID = Guid.NewGuid(), OnlineID = 12 }; var otherInfo = new BeatmapSetInfo { OnlineID = 12 }; Assert.AreNotEqual(ourInfo, otherInfo); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index f637c715a1..06ef3278ab 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.SongSelect private RulesetStore rulesets; private readonly Stack selectedSets = new Stack(); - private readonly HashSet eagerSelectedIDs = new HashSet(); + private readonly HashSet eagerSelectedIDs = new HashSet(); private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo; diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 37f110e727..586cd273be 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -585,7 +585,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestHideSetSelectsCorrectBeatmap() { - int? previousID = null; + Guid? previousID = null; createSongSelect(); addRulesetImportStep(0); AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last())); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index a436fc0bfa..d9ba443eca 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -1,6 +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; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -59,10 +60,9 @@ namespace osu.Game.Tests.Visual.UserInterface Scope = BeatmapLeaderboardScope.Local, BeatmapInfo = new BeatmapInfo { - ID = 1, + ID = Guid.NewGuid(), Metadata = new BeatmapMetadata { - ID = 1, Title = "TestSong", Artist = "TestArtist", Author = new APIUser From 37673f4cf84b4edd49703aebadf96680ac355a54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 14:29:46 +0900 Subject: [PATCH 008/227] Update sets of `BeatmapSet.Metadata` to instead create a `Beatmap` --- .../Online/TestSceneBeatmapDownloading.cs | 17 ++++++++++++----- .../SongSelect/TestSceneBeatmapCarousel.cs | 19 +++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs index 4e77973655..ad9ea79646 100644 --- a/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs +++ b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Tests.Visual; @@ -20,13 +21,19 @@ namespace osu.Game.Tests.Online private static readonly BeatmapSetInfo test_db_model = new BeatmapSetInfo { OnlineID = 1, - Metadata = new BeatmapMetadata + Beatmaps = { - Artist = "test author", - Title = "test title", - Author = new APIUser + new BeatmapInfo { - Username = "mapper" + Metadata = new BeatmapMetadata + { + Artist = "test author", + Title = "test title", + Author = new RealmUser + { + Username = "mapper" + } + } } } }; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 06ef3278ab..7db71fb21e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -409,10 +410,10 @@ namespace osu.Game.Tests.Visual.SongSelect var set = TestResources.CreateTestBeatmapSetInfo(); if (i == 4) - set.Metadata.Artist = zzz_string; + set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string); if (i == 16) - set.Metadata.AuthorString = zzz_string; + set.Beatmaps.ForEach(b => b.Metadata.AuthorString = zzz_string); sets.Add(set); } @@ -432,13 +433,19 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 20; i++) { + // index + 1 because we are using OnlineID which should never be zero. var set = TestResources.CreateTestBeatmapSetInfo(); - set.Metadata.Artist = "same artist"; - set.Metadata.Title = "same title"; + + // only need to set the first as they are a shared reference. + var beatmap = set.Beatmaps.First(); + + beatmap.Metadata.Artist = "same artist"; + beatmap.Metadata.Title = "same title"; + sets.Add(set); } - int idOffset = sets.First().OnlineID ?? 0; + int idOffset = sets.First().OnlineID; loadBeatmaps(sets); @@ -674,7 +681,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Restore different ruleset filter", () => { carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false); - eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.OnlineID ?? -1); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); }); AddAssert("selection changed", () => !carousel.SelectedBeatmapInfo.Equals(manySets.First().Beatmaps.First())); From 213d89b479e83bcc03bdeb38c5dee944f8ee68a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 14:55:41 +0900 Subject: [PATCH 009/227] Update null fallback cases involving `OnlineID` --- .../Visual/Gameplay/TestScenePlayerScoreSubmission.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs | 2 +- .../TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- .../TestSceneMultiplayerGameplayLeaderboardTeams.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiplayerResults.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs | 2 +- osu.Game/Beatmaps/BeatmapModelManager.cs | 8 ++++---- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 6 +++--- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 4 ++-- osu.Game/Stores/BeatmapImporter.cs | 4 ++-- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index cf5aadde6d..9f34931f54 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay createPlayerTest(false, r => { var beatmap = createTestBeatmap(r); - beatmap.BeatmapInfo.OnlineID = null; + beatmap.BeatmapInfo.OnlineID = -1; return beatmap; }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 242eca0bbc..d8964c531f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("import beatmap", () => { importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely(); - importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineID ?? -1; + importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineID; }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index 61058bc87a..c78d1a2f62 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { importedSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely(); importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); - importedBeatmapId = importedBeatmap.OnlineID ?? -1; + importedBeatmapId = importedBeatmap.OnlineID; } [SetUp] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 07a8ef66e1..9b8e67b07a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int user in users) { - SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID ?? 0); + SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true)); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 1237a21e94..8a78c12042 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer foreach (int user in users) { - SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID ?? 0); + SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); var roomUser = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true); roomUser.MatchState = new TeamVersusUserState diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs index 4674601f28..44a1745eee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer PlaylistItem playlistItem = new PlaylistItem { - BeatmapID = beatmapInfo.OnlineID ?? -1, + BeatmapID = beatmapInfo.OnlineID, }; Stack.Push(screen = new MultiplayerResultsScreen(score, 1, playlistItem)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index f5df8d7507..dfc16c44f2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer PlaylistItem playlistItem = new PlaylistItem { - BeatmapID = beatmapInfo.OnlineID ?? -1, + BeatmapID = beatmapInfo.OnlineID, }; SortedDictionary teamScores = new SortedDictionary diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index d0c41e0fb8..2a9b215f32 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps { if (beatmapSet.OnlineID != null) { - beatmapSet.OnlineID = null; + beatmapSet.OnlineID = -1; LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); } } @@ -122,9 +122,9 @@ namespace osu.Game.Beatmaps Delete(existingSetWithSameOnlineID); // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. - existingSetWithSameOnlineID.OnlineID = null; + existingSetWithSameOnlineID.OnlineID = -1; foreach (var b in existingSetWithSameOnlineID.Beatmaps) - b.OnlineID = null; + b.OnlineID = -1; LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineID}). It has been deleted."); } @@ -159,7 +159,7 @@ namespace osu.Game.Beatmaps } } - void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = null); + void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = -1); } /// diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 76232c2932..3c4da12833 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -103,7 +103,7 @@ namespace osu.Game.Beatmaps void fail(Exception e) { - beatmapInfo.OnlineID = null; + beatmapInfo.OnlineID = -1; logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})"); } } @@ -161,7 +161,7 @@ namespace osu.Game.Beatmaps if (string.IsNullOrEmpty(beatmapInfo.MD5Hash) && string.IsNullOrEmpty(beatmapInfo.Path) - && beatmapInfo.OnlineID == null) + && beatmapInfo.OnlineID <= 0) return false; try @@ -175,7 +175,7 @@ namespace osu.Game.Beatmaps cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path"; cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID ?? (object)DBNull.Value)); + cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID)); cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path)); using (var reader = cmd.ExecuteReader()) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 49853418d6..ebdc882d2f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -133,8 +133,8 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.DifficultyName}")); if (!string.IsNullOrEmpty(beatmap.Metadata.Source)) writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); if (!string.IsNullOrEmpty(beatmap.Metadata.Tags)) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); - if (beatmap.BeatmapInfo.OnlineID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineID}")); - if (beatmap.BeatmapInfo.BeatmapSet?.OnlineID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineID}")); + if (beatmap.BeatmapInfo.OnlineID > 0) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineID}")); + if (beatmap.BeatmapInfo.BeatmapSet?.OnlineID > 0) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineID}")); } private void handleDifficulty(TextWriter writer) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d0f9d835fd..2eb226df70 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -238,8 +238,8 @@ namespace osu.Game.Screens.Select.Carousel if (editRequested != null) items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmapInfo))); - if (beatmapInfo.OnlineID.HasValue && beatmapOverlay != null) - items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID.Value))); + if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null) + items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID))); if (collectionManager != null) { diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 69c136b51c..de92da394d 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -240,7 +240,7 @@ namespace osu.Game.Stores ArtistUnicode = decoded.Metadata.ArtistUnicode, Author = { - OnlineID = decoded.Metadata.Author.Id, + OnlineID = decoded.Metadata.Author.OnlineID, Username = decoded.Metadata.Author.Username }, Source = decoded.Metadata.Source, @@ -254,7 +254,7 @@ namespace osu.Game.Stores { Hash = hash, DifficultyName = decodedInfo.DifficultyName, - OnlineID = decodedInfo.OnlineID ?? -1, + OnlineID = decodedInfo.OnlineID, AudioLeadIn = decodedInfo.AudioLeadIn, StackLeniency = decodedInfo.StackLeniency, SpecialStyle = decodedInfo.SpecialStyle, From fda529de2674be00a7b386c23df37f2e092ed762 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 15:52:55 +0900 Subject: [PATCH 010/227] Update usages of `APIUser` to `RealmUser` --- osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs | 6 +++--- .../Multiplayer/TestSceneDrawableRoomPlaylist.cs | 4 ++-- .../Ranking/TestSceneExpandedPanelMiddleContent.cs | 12 ++++++------ .../UserInterface/TestSceneDeleteLocalScore.cs | 3 ++- osu.Game/Beatmaps/BeatmapManager.cs | 7 ++++++- .../Online/API/Requests/Responses/APIBeatmapSet.cs | 8 ++++++-- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs index 4a7d7505ad..10cac4ed9d 100644 --- a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs +++ b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Models; namespace osu.Game.Tests.Beatmaps { @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps { Artist = "artist", Title = "title", - Author = new APIUser { Username = "creator" } + Author = new RealmUser { Username = "creator" } } }; @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Beatmaps { Artist = "artist", Title = "title", - Author = new APIUser { Username = "creator" } + Author = new RealmUser { Username = "creator" } }, DifficultyName = "difficulty" }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 147bbf2626..1607750d15 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -17,7 +17,7 @@ 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.Models; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -305,7 +305,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Metadata = new BeatmapMetadata { Artist = "Artist", - Author = new APIUser { Username = "Creator name here" }, + Author = new RealmUser { Username = "Creator name here" }, Title = "Long title used to check background colour", }, BeatmapSet = new BeatmapSetInfo() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 2cb4fb6b6b..8b646df362 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -12,7 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestMapWithKnownMapper() { - var author = new APIUser { Username = "mapper_name" }; + var author = new RealmUser { Username = "mapper_name" }; AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(author)))); } @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Ranking { AddStep("show excess mods score", () => { - var author = new APIUser { Username = "mapper_name" }; + var author = new RealmUser { Username = "mapper_name" }; var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author)); score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray(); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestMapWithUnknownMapper() { - AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(new APIUser())))); + AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser())))); AddAssert("mapped by text not present", () => this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by"))); @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Ranking var ruleset = new OsuRuleset(); var mods = new Mod[] { ruleset.GetAutoplayMod() }; - var beatmap = createTestBeatmap(new APIUser()); + var beatmap = createTestBeatmap(new RealmUser()); var score = TestResources.CreateTestScoreInfo(beatmap); @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Ranking private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score); - private BeatmapInfo createTestBeatmap([NotNull] APIUser author) + private BeatmapInfo createTestBeatmap([NotNull] RealmUser author) { var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)).BeatmapInfo; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index d9ba443eca..ecc8f697c7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; +using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -65,7 +66,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Title = "TestSong", Artist = "TestArtist", - Author = new APIUser + Author = new RealmUser { Username = "TestAuthor" }, diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index ed7fe0bc91..dc7b5c7f29 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -18,6 +18,7 @@ using osu.Framework.Testing; using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Archives; +using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; @@ -73,7 +74,11 @@ namespace osu.Game.Beatmaps { var metadata = new BeatmapMetadata { - Author = user, + Author = new RealmUser + { + OnlineID = user.OnlineID, + Username = user.Username, + } }; var set = new BeatmapSetInfo diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 57c45faed3..d99c13b977 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; +using osu.Game.Models; #nullable enable @@ -123,8 +124,11 @@ namespace osu.Game.Online.API.Requests.Responses TitleUnicode = TitleUnicode, Artist = Artist, ArtistUnicode = ArtistUnicode, - AuthorID = AuthorID, - Author = Author, + Author = new RealmUser + { + OnlineID = Author.OnlineID, + Username = Author.Username + }, Source = Source, Tags = Tags, }; From 6a671b0a52fb4e3656e83e2ab4e94d5fcf117155 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 16:22:31 +0900 Subject: [PATCH 011/227] Remove unnecessary assigns of `BeatmapSetInfo.Metadata` --- .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 1 - osu.Game.Tests/Resources/TestResources.cs | 1 - .../Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 1 - osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs | 1 - osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs | 1 - osu.Game/Beatmaps/BeatmapManager.cs | 1 - 6 files changed, 6 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index a7b431fb6e..48b22788a7 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -154,7 +154,6 @@ namespace osu.Game.Tests.Online Debug.Assert(info.BeatmapSet != null); info.BeatmapSet.Beatmaps.Add(info); - info.BeatmapSet.Metadata = info.Metadata; info.MD5Hash = stream.ComputeMD5Hash(); info.Hash = stream.ComputeSHA2Hash(); } diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 445394fc77..c16f71334f 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -97,7 +97,6 @@ namespace osu.Game.Tests.Resources OnlineID = setId, Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), DateAdded = DateTimeOffset.UtcNow, - Metadata = metadata }; foreach (var b in getBeatmaps(difficultyCount ?? RNG.Next(1, 20))) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index bd4b38b9c0..7f7c97ac48 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -58,7 +58,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { OnlineID = 10, Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - Metadata = metadata, DateAdded = DateTimeOffset.UtcNow }; diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 6420e7b849..81aacf61cd 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -109,7 +109,6 @@ namespace osu.Game.Tests.Visual.Navigation { Hash = Guid.NewGuid().ToString(), OnlineID = i, - Metadata = metadata, Beatmaps = { new BeatmapInfo diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 5dc1808c12..dbff5964df 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -40,7 +40,6 @@ namespace osu.Game.Tests.Visual.Navigation { Hash = Guid.NewGuid().ToString(), OnlineID = 1, - Metadata = metadata, Beatmaps = { new BeatmapInfo diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index dc7b5c7f29..c558a807e5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -83,7 +83,6 @@ namespace osu.Game.Beatmaps var set = new BeatmapSetInfo { - Metadata = metadata, Beatmaps = { new BeatmapInfo From 2cb97dd5994ae887007e941409e92a495c5c9e10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 17:11:29 +0900 Subject: [PATCH 012/227] Remove unnecessary assigns of EF foreign `ID` fields in tests --- osu.Game.Tests/Resources/TestResources.cs | 1 - osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 -- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 1 - 3 files changed, 4 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index c16f71334f..cd8b031f64 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -131,7 +131,6 @@ namespace osu.Game.Tests.Resources Length = length, BPM = bpm, Ruleset = rulesetInfo, - RulesetID = rulesetInfo.ID ?? -1, Metadata = metadata, BaseDifficulty = new BeatmapDifficulty { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 7db71fb21e..87192badd6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -584,7 +584,6 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i <= 2; i++) { testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); - testMixed.Beatmaps[i].RulesetID = i; } carousel.UpdateBeatmapSet(testMixed); @@ -606,7 +605,6 @@ namespace osu.Game.Tests.Visual.SongSelect testSingle.Beatmaps.ForEach(b => { b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); - b.RulesetID = b.Ruleset.ID ?? 1; }); carousel.UpdateBeatmapSet(testSingle); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index ecc8f697c7..cdb81f059b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -95,7 +95,6 @@ namespace osu.Game.Tests.Visual.UserInterface { OnlineID = i, BeatmapInfo = beatmapInfo, - BeatmapInfoID = beatmapInfo.ID, Accuracy = RNG.NextDouble(), TotalScore = RNG.Next(1, 1000000), MaxCombo = RNG.Next(1, 1000), From df088f96f476b1e80fe9b66c60b83e72548edc83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 17:12:26 +0900 Subject: [PATCH 013/227] Fix incorrect `Metadata`-related null checks --- osu.Game/Beatmaps/WorkingBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 8289b32d31..397d47c389 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps public readonly BeatmapSetInfo BeatmapSetInfo; // TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly). - public BeatmapMetadata Metadata => BeatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); + public BeatmapMetadata Metadata => BeatmapInfo.Metadata; public Waveform Waveform => waveform.Value; @@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps this.audioManager = audioManager; BeatmapInfo = beatmapInfo; - BeatmapSetInfo = beatmapInfo.BeatmapSet; + BeatmapSetInfo = beatmapInfo.BeatmapSet ?? new BeatmapSetInfo(); waveform = new Lazy(GetWaveform); storyboard = new Lazy(GetStoryboard); From e6fdd0e9697146bf2aab7a90015ba6009d8dcde3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 16:10:13 +0900 Subject: [PATCH 014/227] Miscellaneous fixes that don't fit elsewhere --- osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs | 1 - .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 6 +----- osu.Game/OsuGame.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 3 ++- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index d57b3dec5d..e916e8d9ac 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -37,7 +37,6 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapInfo exampleBeatmapInfo => new BeatmapInfo { - RulesetID = 0, Ruleset = rulesets.AvailableRulesets.First(), BaseDifficulty = new BeatmapDifficulty { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 87192badd6..28f5c3ff60 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -378,11 +378,7 @@ namespace osu.Game.Tests.Visual.SongSelect var rulesetBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1); var taikoRuleset = rulesets.AvailableRulesets.ElementAt(1); - rulesetBeatmapSet.Beatmaps.ForEach(b => - { - b.Ruleset = taikoRuleset; - b.RulesetID = 1; - }); + rulesetBeatmapSet.Beatmaps.ForEach(b => b.Ruleset = taikoRuleset); sets.Add(rulesetBeatmapSet); }); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c5a465ae96..0d0a572663 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -458,7 +458,7 @@ namespace osu.Game // Use all beatmaps if predicate matched nothing if (beatmaps.Count == 0) - beatmaps = databasedSet.Beatmaps; + beatmaps = databasedSet.Beatmaps.ToList(); // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection. var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 6791565828..b37877b35f 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Select private void load(OsuColour colours, LocalisationManager localisation, BeatmapDifficultyCache difficultyCache) { var beatmapInfo = working.BeatmapInfo; - var metadata = beatmapInfo.Metadata ?? working.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); + var metadata = beatmapInfo.Metadata; RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index fa90a00f0d..9502dd8968 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -96,7 +96,8 @@ namespace osu.Game.Storyboards public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) { Drawable drawable = null; - string storyboardPath = BeatmapInfo.BeatmapSet?.Files.Find(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath(); + + string storyboardPath = BeatmapInfo.BeatmapSet?.Files.FirstOrDefault(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath(); if (!string.IsNullOrEmpty(storyboardPath)) drawable = new Sprite { Texture = textureStore.Get(storyboardPath) }; From 83cbee39de86fe230967a7ec6b51eff991bcc241 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 17:59:54 +0900 Subject: [PATCH 015/227] Mark cases where `BeatmapSet` is generally guaranteed to be non-null --- osu.Game/Screens/Edit/Editor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 48489c60ab..7955464590 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework; @@ -777,6 +778,8 @@ namespace osu.Game.Screens.Edit var beatmapSet = beatmapManager.QueryBeatmapSet(bs => bs.ID == Beatmap.Value.BeatmapSetInfo.ID) ?? playableBeatmap.BeatmapInfo.BeatmapSet; + Debug.Assert(beatmapSet != null); + var difficultyItems = new List(); foreach (var rulesetBeatmaps in beatmapSet.Beatmaps.GroupBy(b => b.RulesetID).OrderBy(group => group.Key)) From 89d6ffa7f384e333832a267b8f120580237cd4fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 13:24:26 +0900 Subject: [PATCH 016/227] Use `RealmContextFactory` instead of EF --- ...ceneOnlinePlayBeatmapAvailabilityTracker.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 18 ++++++------------ 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 48b22788a7..2cab823526 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Online public Task> CurrentImportTask { get; private set; } - public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) { } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c558a807e5..59adcfca60 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps private readonly WorkingBeatmapCache workingBeatmapCache; private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue; - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) + public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) { var userResources = new FileStore(contextFactory, storage).Store; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 39cd28cad2..9f248ffdca 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Scoring private readonly OsuConfigManager configManager; private readonly ScoreModelManager scoreModelManager; - public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IDatabaseContextFactory contextFactory, Scheduler scheduler, + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory, Scheduler scheduler, IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) { this.scheduler = scheduler; diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 6b029729ea..c631286d11 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -73,9 +73,9 @@ namespace osu.Game.Tests.Visual /// /// In interactive runs (ie. VisualTests) this will use the user's database if is not set to true. /// - protected DatabaseContextFactory ContextFactory => contextFactory.Value; + protected RealmContextFactory ContextFactory => contextFactory.Value; - private Lazy contextFactory; + private Lazy contextFactory; /// /// Whether a fresh storage should be initialised per test (method) run. @@ -117,14 +117,7 @@ namespace osu.Game.Tests.Visual Resources = parent.Get().Resources; - contextFactory = new Lazy(() => - { - var factory = new DatabaseContextFactory(LocalStorage); - - using (var usage = factory.Get()) - usage.Migrate(); - return factory; - }); + contextFactory = new Lazy(() => new RealmContextFactory(LocalStorage, "client")); RecycleLocalStorage(false); @@ -301,8 +294,9 @@ namespace osu.Game.Tests.Visual if (MusicController?.TrackLoaded == true) MusicController.Stop(); - if (contextFactory?.IsValueCreated == true) - contextFactory.Value.ResetDatabase(); + // TODO: what should we do here, if anything? should we use an in-memory realm in this instance? + // if (contextFactory?.IsValueCreated == true) + // contextFactory.Value.ResetDatabase(); RecycleLocalStorage(true); } From 3ecd889fef5f1b9a7882d40148795ac330ed4c2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 13:00:33 +0900 Subject: [PATCH 017/227] Replace EF `RulesetStore` with realm version Pass full EF context factory to `RealmContextFactory` for migration purposes --- .../Database/BeatmapImporterTests.cs | 42 +-- osu.Game.Tests/Database/RulesetStoreTests.cs | 9 +- osu.Game/Database/RealmContextFactory.cs | 1 + osu.Game/OsuGameBase.cs | 10 +- .../Sections/Input/RulesetBindingsSection.cs | 3 + osu.Game/Rulesets/RulesetStore.cs | 164 ++++++----- osu.Game/Stores/RealmRulesetStore.cs | 269 ------------------ 7 files changed, 123 insertions(+), 375 deletions(-) delete mode 100644 osu.Game/Stores/RealmRulesetStore.cs diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 37c2007681..adee545c1d 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using (var importer = new BeatmapImporter(realmFactory, storage)) - using (new RealmRulesetStore(realmFactory, storage)) + using (new RulesetStore(realmFactory, storage)) { ILive? imported; @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); await LoadOszIntoStore(importer, realmFactory.Context); }); @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? tempPath = TestResources.GetTestBeatmapForImport(); @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); @@ -164,7 +164,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -265,7 +265,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -313,7 +313,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -362,7 +362,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -395,7 +395,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var progressNotification = new ImportProgressNotification(); @@ -431,7 +431,7 @@ namespace osu.Game.Tests.Database }; using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -481,7 +481,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -529,7 +529,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -555,7 +555,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var metadata = new BeatmapMetadata { @@ -601,7 +601,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); using (File.OpenRead(temp)) @@ -618,7 +618,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -654,7 +654,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -696,7 +696,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -747,7 +747,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new BeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); await importer.Import(temp); diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index d2de31d11a..4416da6f92 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Game.Rulesets; -using osu.Game.Stores; namespace osu.Game.Tests.Database { @@ -15,7 +14,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, storage) => { - var rulesets = new RealmRulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realmFactory, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, realmFactory.Context.All().Count()); @@ -27,8 +26,8 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, storage) => { - var rulesets = new RealmRulesetStore(realmFactory, storage); - var rulesets2 = new RealmRulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realmFactory, storage); + var rulesets2 = new RulesetStore(realmFactory, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); @@ -43,7 +42,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, storage) => { - var rulesets = new RealmRulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realmFactory, storage); Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged); Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged); diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 946dda1f39..968caaa7ce 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -17,6 +17,7 @@ using osu.Game.Input.Bindings; using osu.Game.Models; using osu.Game.Skinning; using osu.Game.Stores; +using osu.Game.Rulesets; using Realms; #nullable enable diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9256514a0a..144ee17c29 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -40,7 +40,6 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; -using osu.Game.Stores; using osu.Game.Utils; using RuntimeInfo = osu.Framework.RuntimeInfo; @@ -166,8 +165,6 @@ namespace osu.Game private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(global_track_volume_adjust); - private RealmRulesetStore realmRulesetStore; - public OsuGameBase() { UseDevelopmentServer = DebugUtils.IsDebugBuild; @@ -231,6 +228,8 @@ namespace osu.Game dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); + dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); + // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() 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)); @@ -238,11 +237,6 @@ namespace osu.Game 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); - - dependencies.Cache(realmRulesetStore); - // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to // allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete. diff --git a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs index b5d26d4887..dae276c711 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs @@ -1,6 +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.Diagnostics; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -26,6 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input var r = ruleset.CreateInstance(); + Debug.Assert(r != null); + foreach (int variant in r.AvailableVariants) Add(new VariantBindingsSubsection(ruleset, variant)); } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 5cc6a75f43..de3bb1ac34 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -7,24 +7,33 @@ using System.IO; using System.Linq; using System.Reflection; using osu.Framework; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; +#nullable enable + namespace osu.Game.Rulesets { - public class RulesetStore : DatabaseBackedStore, IRulesetStore, IDisposable + public class RulesetStore : IDisposable { - private const string ruleset_library_prefix = "osu.Game.Rulesets"; + private readonly RealmContextFactory realmFactory; + + private const string ruleset_library_prefix = @"osu.Game.Rulesets"; private readonly Dictionary loadedAssemblies = new Dictionary(); - private readonly Storage rulesetStorage; + /// + /// All available rulesets. + /// + public IEnumerable AvailableRulesets => availableRulesets; - public RulesetStore(IDatabaseContextFactory factory, Storage storage = null) - : base(factory) + private readonly List availableRulesets = new List(); + + public RulesetStore(RealmContextFactory realmFactory, Storage? storage = null) { - rulesetStorage = storage?.GetStorageForDirectory("rulesets"); + this.realmFactory = realmFactory; // On android in release configuration assemblies are loaded from the apk directly into memory. // We cannot read assemblies from cwd, so should check loaded assemblies instead. @@ -40,7 +49,11 @@ namespace osu.Game.Rulesets // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail // to load as unable to locate the game core assembly. AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; - loadUserRulesets(); + + var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets"); + if (rulesetStorage != null) + loadUserRulesets(rulesetStorage); + addMissingRulesets(); } @@ -49,21 +62,16 @@ namespace osu.Game.Rulesets /// /// The ruleset's internal ID. /// A ruleset, if available, else null. - public RulesetInfo GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.ID == id); + public RulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id); /// /// Retrieve a ruleset using a known short name. /// /// The ruleset's short name. /// A ruleset, if available, else null. - public RulesetInfo GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); + public RulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); - /// - /// All available rulesets. - /// - public IEnumerable AvailableRulesets { get; private set; } - - private Assembly resolveRulesetDependencyAssembly(object sender, ResolveEventArgs args) + private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args) { var asm = new AssemblyName(args.Name); @@ -72,7 +80,14 @@ namespace osu.Game.Rulesets // already loaded in the AppDomain. var domainAssembly = AppDomain.CurrentDomain.GetAssemblies() // Given name is always going to be equally-or-more qualified than the assembly name. - .Where(a => args.Name.Contains(a.GetName().Name, StringComparison.Ordinal)) + .Where(a => + { + string? name = a.GetName().Name; + if (name == null) + return false; + + return args.Name.Contains(name, StringComparison.Ordinal); + }) // Pick the greatest assembly version. .OrderByDescending(a => a.GetName().Version) .FirstOrDefault(); @@ -85,69 +100,73 @@ namespace osu.Game.Rulesets private void addMissingRulesets() { - using (var usage = ContextFactory.GetForWrite()) + using (var context = realmFactory.CreateContext()) { - var context = usage.Context; - - var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r)).ToList(); - - // add all legacy rulesets first to ensure they have exclusive choice of primary key. - foreach (var r in instances.Where(r => r is ILegacyRuleset)) + context.Write(realm => { - if (context.RulesetInfo.SingleOrDefault(dbRuleset => dbRuleset.ID == r.RulesetInfo.ID) == null) - context.RulesetInfo.Add(r.RulesetInfo); - } + var rulesets = realm.All(); - context.SaveChanges(); + List instances = loadedAssemblies.Values + .Select(r => Activator.CreateInstance(r) as Ruleset) + .Where(r => r != null) + .Select(r => r.AsNonNull()) + .ToList(); - var existingRulesets = context.RulesetInfo.ToList(); - - // add any other rulesets which have assemblies present but are not yet in the database. - foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) - { - if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) + // add all legacy rulesets first to ensure they have exclusive choice of primary key. + foreach (var r in instances.Where(r => r is ILegacyRuleset)) { - var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); + if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null) + realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); + } - if (existingSameShortName != null) + // add any other rulesets which have assemblies present but are not yet in the database. + foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) + { + if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) { - // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. - // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. - // in such cases, update the instantiation info of the existing entry to point to the new one. - existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; + var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); + + if (existingSameShortName != null) + { + // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. + // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. + // in such cases, update the instantiation info of the existing entry to point to the new one. + existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; + } + else + realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); } - else - context.RulesetInfo.Add(r.RulesetInfo); } - } - context.SaveChanges(); + List detachedRulesets = new List(); - // perform a consistency check - foreach (var r in context.RulesetInfo) - { - try + // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. + foreach (var r in rulesets) { - var resolvedType = Type.GetType(r.InstantiationInfo) - ?? throw new RulesetLoadException(@"Type could not be resolved"); + try + { + var resolvedType = Type.GetType(r.InstantiationInfo) + ?? throw new RulesetLoadException(@"Type could not be resolved"); - var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo - ?? throw new RulesetLoadException(@"Instantiation failure"); + var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo + ?? throw new RulesetLoadException(@"Instantiation failure"); - r.Name = instanceInfo.Name; - r.ShortName = instanceInfo.ShortName; - r.InstantiationInfo = instanceInfo.InstantiationInfo; - r.Available = true; + r.Name = instanceInfo.Name; + r.ShortName = instanceInfo.ShortName; + r.InstantiationInfo = instanceInfo.InstantiationInfo; + r.Available = true; + + detachedRulesets.Add(r.Clone()); + } + catch (Exception ex) + { + r.Available = false; + Logger.Log($"Could not load ruleset {r}: {ex.Message}"); + } } - catch - { - r.Available = false; - } - } - context.SaveChanges(); - - AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList(); + availableRulesets.AddRange(detachedRulesets); + }); } } @@ -155,22 +174,23 @@ namespace osu.Game.Rulesets { foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies()) { - string rulesetName = ruleset.GetName().Name; + string? rulesetName = ruleset.GetName().Name; - if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || ruleset.GetName().Name.Contains("Tests")) + if (rulesetName == null) + continue; + + if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests")) continue; addRuleset(ruleset); } } - private void loadUserRulesets() + private void loadUserRulesets(Storage rulesetStorage) { - if (rulesetStorage == null) return; + var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll"); - var rulesets = rulesetStorage.GetFiles(".", $"{ruleset_library_prefix}.*.dll"); - - foreach (string ruleset in rulesets.Where(f => !f.Contains("Tests"))) + foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests"))) loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset)); } @@ -178,7 +198,7 @@ namespace osu.Game.Rulesets { try { - string[] files = Directory.GetFiles(RuntimeInfo.StartupDirectory, $"{ruleset_library_prefix}.*.dll"); + string[] files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll"); foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests"))) loadRulesetFromFile(file); @@ -191,7 +211,7 @@ namespace osu.Game.Rulesets private void loadRulesetFromFile(string file) { - string filename = Path.GetFileNameWithoutExtension(file); + string? filename = Path.GetFileNameWithoutExtension(file); if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) return; diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs deleted file mode 100644 index f208b392cb..0000000000 --- a/osu.Game/Stores/RealmRulesetStore.cs +++ /dev/null @@ -1,269 +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 System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using osu.Framework; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Game.Database; -using osu.Game.Rulesets; - -#nullable enable - -namespace osu.Game.Stores -{ - public class RealmRulesetStore : IRulesetStore, IDisposable - { - private readonly RealmContextFactory realmFactory; - - private const string ruleset_library_prefix = @"osu.Game.Rulesets"; - - private readonly Dictionary loadedAssemblies = new Dictionary(); - - /// - /// All available rulesets. - /// - public IEnumerable AvailableRulesets => availableRulesets; - - private readonly List availableRulesets = new List(); - - public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null) - { - this.realmFactory = realmFactory; - - // On android in release configuration assemblies are loaded from the apk directly into memory. - // We cannot read assemblies from cwd, so should check loaded assemblies instead. - loadFromAppDomain(); - - // This null check prevents Android from attempting to load the rulesets from disk, - // as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android. - // See https://github.com/xamarin/xamarin-android/issues/3489. - if (RuntimeInfo.StartupDirectory != null) - loadFromDisk(); - - // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory. - // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail - // to load as unable to locate the game core assembly. - AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; - - var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets"); - if (rulesetStorage != null) - loadUserRulesets(rulesetStorage); - - addMissingRulesets(); - } - - /// - /// Retrieve a ruleset using a known ID. - /// - /// The ruleset's internal ID. - /// A ruleset, if available, else null. - public RealmRuleset? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id); - - /// - /// Retrieve a ruleset using a known short name. - /// - /// The ruleset's short name. - /// A ruleset, if available, else null. - public RealmRuleset? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); - - private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args) - { - var asm = new AssemblyName(args.Name); - - // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies. - // this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name - // already loaded in the AppDomain. - var domainAssembly = AppDomain.CurrentDomain.GetAssemblies() - // Given name is always going to be equally-or-more qualified than the assembly name. - .Where(a => - { - string? name = a.GetName().Name; - if (name == null) - return false; - - return args.Name.Contains(name, StringComparison.Ordinal); - }) - // Pick the greatest assembly version. - .OrderByDescending(a => a.GetName().Version) - .FirstOrDefault(); - - if (domainAssembly != null) - return domainAssembly; - - return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName); - } - - private void addMissingRulesets() - { - using (var context = realmFactory.CreateContext()) - { - context.Write(realm => - { - var rulesets = realm.All(); - - List instances = loadedAssemblies.Values - .Select(r => Activator.CreateInstance(r) as Ruleset) - .Where(r => r != null) - .Select(r => r.AsNonNull()) - .ToList(); - - // add all legacy rulesets first to ensure they have exclusive choice of primary key. - foreach (var r in instances.Where(r => r is ILegacyRuleset)) - { - if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null) - realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); - } - - // add any other rulesets which have assemblies present but are not yet in the database. - foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) - { - if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) - { - var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); - - if (existingSameShortName != null) - { - // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. - // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. - // in such cases, update the instantiation info of the existing entry to point to the new one. - existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; - } - else - realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID)); - } - } - - List detachedRulesets = new List(); - - // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. - foreach (var r in rulesets) - { - try - { - var resolvedType = Type.GetType(r.InstantiationInfo) - ?? throw new RulesetLoadException(@"Type could not be resolved"); - - var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo - ?? throw new RulesetLoadException(@"Instantiation failure"); - - r.Name = instanceInfo.Name; - r.ShortName = instanceInfo.ShortName; - r.InstantiationInfo = instanceInfo.InstantiationInfo; - r.Available = true; - - detachedRulesets.Add(r.Clone()); - } - catch (Exception ex) - { - r.Available = false; - Logger.Log($"Could not load ruleset {r}: {ex.Message}"); - } - } - - availableRulesets.AddRange(detachedRulesets); - }); - } - } - - private void loadFromAppDomain() - { - foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies()) - { - string? rulesetName = ruleset.GetName().Name; - - if (rulesetName == null) - continue; - - if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests")) - continue; - - addRuleset(ruleset); - } - } - - private void loadUserRulesets(Storage rulesetStorage) - { - var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll"); - - foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests"))) - loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset)); - } - - private void loadFromDisk() - { - try - { - string[] files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll"); - - foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests"))) - loadRulesetFromFile(file); - } - catch (Exception e) - { - Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}"); - } - } - - private void loadRulesetFromFile(string file) - { - string? filename = Path.GetFileNameWithoutExtension(file); - - if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) - return; - - try - { - addRuleset(Assembly.LoadFrom(file)); - } - catch (Exception e) - { - Logger.Error(e, $"Failed to load ruleset {filename}"); - } - } - - private void addRuleset(Assembly assembly) - { - if (loadedAssemblies.ContainsKey(assembly)) - return; - - // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799). - // as a failsafe, also compare by FullName. - if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName)) - return; - - try - { - loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset))); - } - catch (Exception e) - { - Logger.Error(e, $"Failed to add ruleset {assembly}"); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; - } - - #region Implementation of IRulesetStore - - IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id); - IRulesetInfo? IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName); - IEnumerable IRulesetStore.AvailableRulesets => AvailableRulesets; - - #endregion - } -} From 116f35c52ab361a2f3bc4802e656873696698524 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 16:10:50 +0900 Subject: [PATCH 018/227] Remove EF `FileStore` --- .../Visual/Navigation/TestSceneOsuGame.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 3 +- osu.Game/IO/FileStore.cs | 125 ------------------ osu.Game/OsuGameBase.cs | 6 - 4 files changed, 4 insertions(+), 134 deletions(-) delete mode 100644 osu.Game/IO/FileStore.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index 28ff776d5f..99904af42d 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -16,7 +16,6 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Input; using osu.Game.Input.Bindings; -using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Overlays; @@ -25,6 +24,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Skinning; +using osu.Game.Stores; using osu.Game.Utils; namespace osu.Game.Tests.Visual.Navigation @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Navigation typeof(ISkinSource), typeof(IAPIProvider), typeof(RulesetStore), - typeof(FileStore), + typeof(RealmFileStore), typeof(ScoreManager), typeof(BeatmapManager), typeof(IRulesetConfigCache), diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 59adcfca60..a8897adf04 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -24,6 +24,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Skinning; +using osu.Game.Stores; namespace osu.Game.Beatmaps { @@ -42,7 +43,7 @@ namespace osu.Game.Beatmaps public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) { - var userResources = new FileStore(contextFactory, storage).Store; + var userResources = new RealmFileStore(contextFactory, storage).Store; BeatmapTrackStore = audioManager.GetTrackStore(userResources); diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs deleted file mode 100644 index ebe1ebfe69..0000000000 --- a/osu.Game/IO/FileStore.cs +++ /dev/null @@ -1,125 +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 System; -using System.IO; -using System.Linq; -using osu.Framework.Extensions; -using osu.Framework.IO.Stores; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Game.Database; -using osu.Game.Extensions; - -namespace osu.Game.IO -{ - /// - /// Handles the Store and retrieval of Files/FileSets to the database backing - /// - public class FileStore : DatabaseBackedStore - { - public readonly IResourceStore Store; - - public new Storage Storage => base.Storage; - - public FileStore(IDatabaseContextFactory contextFactory, Storage storage) - : base(contextFactory, storage.GetStorageForDirectory(@"files")) - { - Store = new StorageBackedResourceStore(Storage); - } - - public FileInfo Add(Stream data, bool reference = true) - { - using (var usage = ContextFactory.GetForWrite()) - { - string hash = data.ComputeSHA2Hash(); - - var existing = usage.Context.FileInfo.FirstOrDefault(f => f.Hash == hash); - - var info = existing ?? new FileInfo { Hash = hash }; - - string path = info.GetStoragePath(); - - // we may be re-adding a file to fix missing store entries. - bool requiresCopy = !Storage.Exists(path); - - if (!requiresCopy) - { - // even if the file already exists, check the existing checksum for safety. - using (var stream = Storage.GetStream(path)) - requiresCopy |= stream.ComputeSHA2Hash() != hash; - } - - if (requiresCopy) - { - data.Seek(0, SeekOrigin.Begin); - - using (var output = Storage.GetStream(path, FileAccess.Write)) - data.CopyTo(output); - - data.Seek(0, SeekOrigin.Begin); - } - - if (reference || existing == null) - Reference(info); - - return info; - } - } - - public void Reference(params FileInfo[] files) - { - if (files.Length == 0) return; - - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; - - foreach (var f in files.GroupBy(f => f.ID)) - { - var refetch = context.Find(f.First().ID) ?? f.First(); - refetch.ReferenceCount += f.Count(); - context.FileInfo.Update(refetch); - } - } - } - - public void Dereference(params FileInfo[] files) - { - if (files.Length == 0) return; - - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; - - foreach (var f in files.GroupBy(f => f.ID)) - { - var refetch = context.FileInfo.Find(f.Key); - refetch.ReferenceCount -= f.Count(); - context.FileInfo.Update(refetch); - } - } - } - - public override void Cleanup() - { - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; - - foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) - { - try - { - Storage.Delete(f.GetStoragePath()); - context.FileInfo.Remove(f); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete beatmap {f}"); - } - } - } - } - } -} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 144ee17c29..065cec0e97 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -143,8 +143,6 @@ namespace osu.Game private UserLookupCache userCache; private BeatmapLookupCache beatmapCache; - private FileStore fileStore; - private RulesetConfigCache rulesetConfigCache; private SpectatorClient spectatorClient; @@ -226,8 +224,6 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); - dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); - dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() @@ -285,8 +281,6 @@ namespace osu.Game dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); - fileStore.Cleanup(); - // add api components to hierarchy. if (API is APIAccess apiAccess) AddInternal(apiAccess); From 4763fe54d6341b1797634dbbdb588a44f1139c07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 16:17:53 +0900 Subject: [PATCH 019/227] Remove unused store classes --- osu.Game/Skinning/SkinStore.cs | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 osu.Game/Skinning/SkinStore.cs diff --git a/osu.Game/Skinning/SkinStore.cs b/osu.Game/Skinning/SkinStore.cs deleted file mode 100644 index 922d146259..0000000000 --- a/osu.Game/Skinning/SkinStore.cs +++ /dev/null @@ -1,16 +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.Framework.Platform; -using osu.Game.Database; - -namespace osu.Game.Skinning -{ - public class SkinStore : MutableDatabaseBackedStoreWithFileIncludes - { - public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null) - : base(contextFactory, storage) - { - } - } -} From b77bb2f12b853bf4be1e18e149984fad747262e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Dec 2021 16:19:38 +0900 Subject: [PATCH 020/227] Switch `BeatmapModelManager` to use `RealmArchiveModelManager` base class --- osu.Game/Beatmaps/BeatmapModelManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 2a9b215f32..b909175d2d 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -24,6 +24,7 @@ using osu.Game.IO.Archives; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Skinning; +using osu.Game.Stores; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -32,7 +33,7 @@ namespace osu.Game.Beatmaps /// Handles ef-core storage of beatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapModelManager : ArchiveModelManager + public class BeatmapModelManager : RealmArchiveModelManager { /// /// Fired when a single difficulty has been hidden. From b8cd3cdbbc2f04b861a879201c828559a6b725ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Nov 2021 12:16:08 +0900 Subject: [PATCH 021/227] Various updates to ruleset and primary key usages to move closer to realm support --- .../Database/BeatmapImporterTests.cs | 2 +- .../UserInterface/TestScenePlaylistOverlay.cs | 2 +- osu.Game/Beatmaps/Beatmap.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 6 ++-- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 22 +++++++-------- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 5 ++-- osu.Game/Database/DatabaseBackedStore.cs | 4 +-- osu.Game/Database/ModelDownloader.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 2 +- osu.Game/OsuGameBase.cs | 28 ++++++------------- osu.Game/Rulesets/EFRulesetInfo.cs | 2 +- osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 2 +- osu.Game/Screens/Play/Player.cs | 7 +++-- osu.Game/Screens/Play/SoloPlayer.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 9 +++--- .../Select/Carousel/CarouselBeatmapSet.cs | 10 +++---- .../Select/Carousel/SetPanelContent.cs | 4 +-- osu.Game/Screens/Select/FilterControl.cs | 5 +--- .../Select/Leaderboards/BeatmapLeaderboard.cs | 4 +-- osu.Game/Screens/Select/SongSelect.cs | 2 +- .../Tests/Beatmaps/HitObjectSampleTest.cs | 13 +++++---- osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 22 files changed, 64 insertions(+), 73 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index adee545c1d..c5dbcf155c 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -505,7 +505,7 @@ namespace osu.Game.Tests.Database RunTestWithRealmAsync(async (realmFactory, storage) => { using var importer = new NonOptimisedBeatmapImporter(realmFactory, storage); - using var store = new RealmRulesetStore(realmFactory, storage); + using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 39146d584c..62f3b63780 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.Centre); }); - AddAssert("song 1 is 5th", () => beatmapSets[4] == first); + AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first)); AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left)); } diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 435183fe92..ccecd69d21 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps } [JsonIgnore] - public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata; + public BeatmapMetadata Metadata => BeatmapInfo.Metadata; public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a8897adf04..5c8cf38682 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : IModelManager, IModelFileManager, IModelImporter, IWorkingBeatmapCache, IDisposable + public class BeatmapManager : IModelManager, IModelFileManager, IModelImporter, IWorkingBeatmapCache, IDisposable { public ITrackStore BeatmapTrackStore { get; } @@ -294,12 +294,12 @@ namespace osu.Game.Beatmaps #region Implementation of IModelFileManager - public void ReplaceFile(BeatmapSetInfo model, BeatmapSetFileInfo file, Stream contents) + public void ReplaceFile(BeatmapSetInfo model, RealmNamedFileUsage file, Stream contents) { beatmapModelManager.ReplaceFile(model, file, contents); } - public void DeleteFile(BeatmapSetInfo model, BeatmapSetFileInfo file) + public void DeleteFile(BeatmapSetInfo model, RealmNamedFileUsage file) { beatmapModelManager.DeleteFile(model, file); } diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 3c4da12833..444bf09487 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -16,6 +17,7 @@ using osu.Framework.Threading; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Stores; using SharpCompress.Compressors; using SharpCompress.Compressors.BZip2; @@ -83,15 +85,14 @@ namespace osu.Game.Beatmaps if (res != null) { beatmapInfo.Status = res.Status; + + Debug.Assert(beatmapInfo.BeatmapSet != null); + beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None; beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID; beatmapInfo.OnlineID = res.OnlineID; - if (beatmapInfo.Metadata != null) - beatmapInfo.Metadata.AuthorID = res.AuthorID; - - if (beatmapInfo.BeatmapSet.Metadata != null) - beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID; + beatmapInfo.Metadata.Author.OnlineID = res.AuthorID; logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}."); } @@ -185,15 +186,14 @@ namespace osu.Game.Beatmaps var status = (BeatmapOnlineStatus)reader.GetByte(2); beatmapInfo.Status = status; + + Debug.Assert(beatmapInfo.BeatmapSet != null); + beatmapInfo.BeatmapSet.Status = status; beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0); beatmapInfo.OnlineID = reader.GetInt32(1); - if (beatmapInfo.Metadata != null) - beatmapInfo.Metadata.AuthorID = reader.GetInt32(3); - - if (beatmapInfo.BeatmapSet.Metadata != null) - beatmapInfo.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); + beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3); logForModel(set, $"Cached local retrieval for {beatmapInfo}."); return true; @@ -211,7 +211,7 @@ namespace osu.Game.Beatmaps } private void logForModel(BeatmapSetInfo set, string message) => - ArchiveModelManager.LogForModel(set, $"[{nameof(BeatmapOnlineLookupQueue)}] {message}"); + RealmArchiveModelImporter.LogForModel(set, $"[{nameof(BeatmapOnlineLookupQueue)}] {message}"); public void Dispose() { diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 514551e184..95983ae052 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -79,7 +79,7 @@ namespace osu.Game.Beatmaps // if there are no files, presume the full beatmap info has not yet been fetched from the database. if (beatmapInfo?.BeatmapSet?.Files.Count == 0) { - int lookupId = beatmapInfo.ID; + var lookupId = beatmapInfo.ID; beatmapInfo = BeatmapManager.QueryBeatmap(b => b.ID == lookupId); } @@ -93,7 +93,8 @@ namespace osu.Game.Beatmaps if (working != null) return working; - beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; + // TODO: is this still required..? + //beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index 03e1c014b2..a37155ee62 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -11,7 +11,7 @@ namespace osu.Game.Database { protected readonly Storage Storage; - protected readonly IDatabaseContextFactory ContextFactory; + protected readonly RealmContextFactory ContextFactory; /// /// Refresh an instance potentially from a different thread with a local context-tracked instance. @@ -36,7 +36,7 @@ namespace osu.Game.Database } } - protected DatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) + protected DatabaseBackedStore(RealmContextFactory contextFactory, Storage storage = null) { ContextFactory = contextFactory; Storage = storage; diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs index 362bc68cc1..2fa3357b06 100644 --- a/osu.Game/Database/ModelDownloader.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -14,7 +14,7 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Database { public abstract class ModelDownloader : IModelDownloader - where TModel : class, IHasPrimaryKey, ISoftDelete, IEquatable, T + where TModel : class, IHasGuidPrimaryKey, ISoftDelete, IEquatable, T where T : class { public Action PostNotification { protected get; set; } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index f95c884fe5..9482f1c0d8 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -78,7 +78,7 @@ namespace osu.Game.Input.Bindings { var defaults = DefaultKeyBindings.ToList(); - if (ruleset != null && !ruleset.ID.HasValue) + if (ruleset != null && !ruleset.IsManaged) // some tests instantiate a ruleset which is not present in the database. // in these cases we still want key bindings to work, but matching to database instances would result in none being present, // so let's populate the defaults directly. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 065cec0e97..c4dcc35b8e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -190,9 +190,6 @@ namespace osu.Game runMigrations(); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); - dependencies.CacheAs(RulesetStore); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory)); new EFToRealmMigrator(contextFactory, realmFactory, LocalConfig).Run(); @@ -227,24 +224,12 @@ namespace osu.Game dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - 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(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, realmFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realmFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API)); - // this should likely be moved to ArchiveModelManager when another case appears where it is necessary - // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to - // allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete. - List getBeatmapScores(BeatmapSetInfo set) - { - var beatmapIds = BeatmapManager.QueryBeatmaps(b => b.BeatmapSetInfoID == set.ID).Select(b => b.ID).ToList(); - return ScoreManager.QueryScores(s => beatmapIds.Contains(s.BeatmapInfo.ID)).ToList(); - } - - BeatmapManager.ItemRemoved += item => ScoreManager.Delete(getBeatmapScores(item), true); - BeatmapManager.ItemUpdated += item => ScoreManager.Undelete(getBeatmapScores(item), true); - dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); AddInternal(difficultyCache); @@ -438,7 +423,9 @@ namespace osu.Game private void onRulesetChanged(ValueChangedEvent r) { - if (r.NewValue?.Available != true) + Ruleset instance; + + if (r.NewValue?.Available != true || (instance = r.NewValue.CreateInstance()) == null) { // reject the change if the ruleset is not available. Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First(); @@ -448,7 +435,9 @@ namespace osu.Game var dict = new Dictionary>(); foreach (ModType type in Enum.GetValues(typeof(ModType))) - dict[type] = r.NewValue.CreateInstance().GetModsFor(type).ToList(); + { + dict[type] = instance.GetModsFor(type).ToList(); + } if (!SelectedMods.Disabled) SelectedMods.Value = Array.Empty(); @@ -489,7 +478,6 @@ namespace osu.Game contextFactory?.FlushConnections(); - realmRulesetStore?.Dispose(); realmFactory?.Dispose(); } } diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index 36e2add557..a7070ad2b6 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets throw new RulesetLoadException(@"Instantiation failure"); // overwrite the pre-populated RulesetInfo with a potentially database attached copy. - ruleset.RulesetInfo = this; + // ruleset.RulesetInfo = this; return ruleset; } diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index 6d7a4a72e2..08643eb8c1 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load() { - InterpretedDifficulty.Default = EditorBeatmap.BeatmapInfo.DifficultyRating; + InterpretedDifficulty.Default = BeatmapDifficultyCache.GetDifficultyRating(EditorBeatmap.BeatmapInfo.StarRating); InterpretedDifficulty.SetDefault(); IssueList = new IssueList(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2a6f5e2398..a66026b423 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -226,8 +226,8 @@ namespace osu.Game.Screens.Play // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; - if (ruleset.RulesetInfo.ID != null) - Score.ScoreInfo.RulesetID = ruleset.RulesetInfo.ID.Value; + if (ruleset.RulesetInfo.OnlineID >= 0) + Score.ScoreInfo.RulesetID = ruleset.RulesetInfo.OnlineID; Score.ScoreInfo.Mods = gameplayMods; dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); @@ -488,6 +488,9 @@ namespace osu.Game.Screens.Play var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; ruleset = rulesetInfo.CreateInstance(); + if (ruleset == null) + throw new RulesetLoadException("Instantiation failure"); + try { playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods); diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index c8d831ebe6..eced2d142b 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateTokenRequest() { - int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID ?? -1; + int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID; int rulesetId = Ruleset.Value.OnlineID; if (beatmapId <= 0) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b0d0821ee9..7e292e2de7 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -197,7 +197,8 @@ namespace osu.Game.Screens.Select public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { - int? previouslySelectedID = null; + Guid? previouslySelectedID = null; + CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.Equals(beatmapSet)); // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required @@ -626,9 +627,9 @@ namespace osu.Game.Screens.Select if (beatmapSet.Beatmaps.All(b => b.Hidden)) return null; - // todo: remove the need for this. - foreach (var b in beatmapSet.Beatmaps) - b.Metadata ??= beatmapSet.Metadata; + // todo: probably not required any more. + // foreach (var b in beatmapSet.Beatmaps) + // b.Metadata ??= beatmapSet.Metadata; var set = new CarouselBeatmapSet(beatmapSet) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 9e411d5daa..a1a8de04ae 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Select.Carousel if (LastSelected == null || LastSelected.Filtered.Value) { if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.BeatmapInfo)) is BeatmapInfo recommended) - return Children.OfType().First(b => b.BeatmapInfo == recommended); + return Children.OfType().First(b => b.BeatmapInfo.Equals(recommended)); } return base.GetNextToSelect(); @@ -63,16 +63,16 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Artist: - return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase); + return string.Compare(BeatmapSet.Metadata?.Artist, otherSet.BeatmapSet.Metadata?.Artist, StringComparison.OrdinalIgnoreCase); case SortMode.Title: - return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase); + return string.Compare(BeatmapSet.Metadata?.Title, otherSet.BeatmapSet.Metadata?.Title, StringComparison.OrdinalIgnoreCase); case SortMode.Author: - return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase); + return string.Compare(BeatmapSet.Metadata?.Author.Username, otherSet.BeatmapSet.Metadata?.Author.Username, StringComparison.OrdinalIgnoreCase); case SortMode.Source: - return string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase); + return string.Compare(BeatmapSet.Metadata?.Source, otherSet.BeatmapSet.Metadata?.Source, StringComparison.OrdinalIgnoreCase); case SortMode.DateAdded: return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index f2054677b0..9aa85c872e 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -41,13 +41,13 @@ namespace osu.Game.Screens.Select.Carousel { new OsuSpriteText { - Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title), + Text = new RomanisableString(beatmapSet.Metadata?.TitleUnicode, beatmapSet.Metadata?.Title), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), Shadow = true, }, new OsuSpriteText { - Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist), + Text = new RomanisableString(beatmapSet.Metadata?.ArtistUnicode, beatmapSet.Metadata?.Artist), Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), Shadow = true, }, diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index e95bd7f653..e54b25873b 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -37,8 +36,6 @@ namespace osu.Game.Screens.Select public FilterCriteria CreateCriteria() { - Debug.Assert(ruleset.Value.ID != null); - string query = searchTextBox.Text; var criteria = new FilterCriteria @@ -56,7 +53,7 @@ namespace osu.Game.Screens.Select if (!maximumStars.IsDefault) criteria.UserStarDifficulty.Max = maximumStars.Value; - criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria(); + criteria.RulesetCriteria = ruleset.Value.CreateInstance()?.CreateRulesetFilterCriteria(); FilterQueryParser.ApplyQueries(criteria, query); return criteria; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 0102986070..3b25d34aec 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Select.Leaderboards get => beatmapInfo; set { - if (beatmapInfo == value) + if (beatmapInfo.Equals(value)) return; beatmapInfo = value; @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (fetchBeatmapInfo.OnlineID == null || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) { PlaceholderState = PlaceholderState.Unavailable; return null; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 08ad9f2ec0..a40e8e2bde 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -482,7 +482,7 @@ namespace osu.Game.Screens.Select else selectionChangedDebounce = Scheduler.AddDelayed(run, 200); - if (beatmap != beatmapInfoPrevious) + if (!beatmap.Equals(beatmapInfoPrevious)) { if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50) { diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 27d1de83ec..4e955975a0 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -78,7 +79,11 @@ namespace osu.Game.Tests.Beatmaps currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader); // populate ruleset for beatmap converters that require it to be present. - currentTestBeatmap.BeatmapInfo.Ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID); + var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID); + + Debug.Assert(ruleset != null); + + currentTestBeatmap.BeatmapInfo.Ruleset = ruleset; }); }); @@ -94,11 +99,7 @@ namespace osu.Game.Tests.Beatmaps userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile)); beatmapInfo.BeatmapSet.Files.Clear(); - beatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo - { - Filename = beatmapFile, - FileInfo = new IO.FileInfo { Hash = beatmapFile } - }); + beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, 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/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 07152b5a3e..a2f567f3b0 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual { private readonly WorkingBeatmap testBeatmap; - public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host, WorkingBeatmap defaultBeatmap, WorkingBeatmap testBeatmap) + public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host, WorkingBeatmap defaultBeatmap, WorkingBeatmap testBeatmap) : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) { this.testBeatmap = testBeatmap; From 8c0db79ec1803485d74849e18551e4c06d4ee21f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Dec 2021 15:59:04 +0900 Subject: [PATCH 022/227] Remove `BeatmapStore` and update surrounding code --- osu.Game/Beatmaps/BeatmapModelManager.cs | 17 ++-- osu.Game/Beatmaps/BeatmapStore.cs | 117 ----------------------- 2 files changed, 8 insertions(+), 126 deletions(-) delete mode 100644 osu.Game/Beatmaps/BeatmapStore.cs diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index b909175d2d..f6f2e16410 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Skinning; using osu.Game.Stores; +using Realms; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -59,24 +60,22 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - private readonly BeatmapStore beatmaps; private readonly RulesetStore rulesets; - public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, GameHost host = null) - : base(storage, contextFactory, new BeatmapStore(contextFactory), host) + public BeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, GameHost host = null) + : base(storage, contextFactory) { this.rulesets = rulesets; - beatmaps = (BeatmapStore)ModelStore; - beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); - beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); + // beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); + // beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); + // beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); + // beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); } protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken) { if (archive != null) beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files)); diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs deleted file mode 100644 index 197581db88..0000000000 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ /dev/null @@ -1,117 +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 System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using osu.Game.Database; - -namespace osu.Game.Beatmaps -{ - /// - /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing - /// - public class BeatmapStore : MutableDatabaseBackedStoreWithFileIncludes - { - public event Action BeatmapHidden; - public event Action BeatmapRestored; - - public BeatmapStore(IDatabaseContextFactory factory) - : base(factory) - { - } - - /// - /// Hide a in the database. - /// - /// The beatmap to hide. - /// Whether the beatmap's was changed. - public bool Hide(BeatmapInfo beatmapInfo) - { - using (ContextFactory.GetForWrite()) - { - Refresh(ref beatmapInfo, Beatmaps); - - if (beatmapInfo.Hidden) return false; - - beatmapInfo.Hidden = true; - } - - BeatmapHidden?.Invoke(beatmapInfo); - return true; - } - - /// - /// Restore a previously hidden . - /// - /// The beatmap to restore. - /// Whether the beatmap's was changed. - public bool Restore(BeatmapInfo beatmapInfo) - { - using (ContextFactory.GetForWrite()) - { - Refresh(ref beatmapInfo, Beatmaps); - - if (!beatmapInfo.Hidden) return false; - - beatmapInfo.Hidden = false; - } - - BeatmapRestored?.Invoke(beatmapInfo); - return true; - } - - protected override IQueryable AddIncludesForDeletion(IQueryable query) => - base.AddIncludesForDeletion(query) - .Include(s => s.Metadata) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata); - - protected override IQueryable AddIncludesForConsumption(IQueryable query) => - base.AddIncludesForConsumption(query) - .Include(s => s.Metadata) - .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata); - - protected override void Purge(List items, OsuDbContext context) - { - // metadata is M-N so we can't rely on cascades - context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata)); - context.BeatmapMetadata.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); - - // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly. - context.BeatmapDifficulty.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); - - base.Purge(items, context); - } - - public IQueryable BeatmapSetsOverview => ContextFactory.Get().BeatmapSetInfo - .Include(s => s.Metadata) - .Include(s => s.Beatmaps) - .AsNoTracking(); - - public IQueryable BeatmapSetsWithoutRuleset => ContextFactory.Get().BeatmapSetInfo - .Include(s => s.Metadata) - .Include(s => s.Files).ThenInclude(f => f.FileInfo) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) - .AsNoTracking(); - - public IQueryable BeatmapSetsWithoutFiles => ContextFactory.Get().BeatmapSetInfo - .Include(s => s.Metadata) - .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) - .AsNoTracking(); - - public IQueryable Beatmaps => - ContextFactory.Get().BeatmapInfo - .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) - .Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo) - .Include(b => b.Metadata) - .Include(b => b.Ruleset) - .Include(b => b.BaseDifficulty); - } -} From 0dd23c46b04caebae04a115ce6c7f1591ae21e6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Nov 2021 23:18:37 +0900 Subject: [PATCH 023/227] Add basic `RealmScore` implementation --- osu.Game/Models/RealmScore.cs | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 osu.Game/Models/RealmScore.cs diff --git a/osu.Game/Models/RealmScore.cs b/osu.Game/Models/RealmScore.cs new file mode 100644 index 0000000000..323bd2393c --- /dev/null +++ b/osu.Game/Models/RealmScore.cs @@ -0,0 +1,68 @@ +// 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 osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Users; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("Score")] + public class RealmScore : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IScoreInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public IList Files { get; } = null!; + + public string Hash { get; set; } = string.Empty; + + public bool DeletePending { get; set; } + + public bool Equals(RealmScore other) => other.ID == ID; + + [Indexed] + public long OnlineID { get; set; } = -1; + + public RealmUser User { get; set; } = null!; + + public long TotalScore { get; set; } + + public int MaxCombo { get; set; } + + public double Accuracy { get; set; } + + public bool HasReplay { get; set; } + + public DateTimeOffset Date { get; set; } + + public double? PP { get; set; } = null; + + public RealmBeatmap Beatmap { get; set; } = null!; + + public RealmRuleset Ruleset { get; set; } = null!; + + public ScoreRank Rank + { + get => (ScoreRank)RankInt; + set => RankInt = (int)value; + } + + [MapTo(nameof(Rank))] + public int RankInt { get; set; } + + IRulesetInfo IScoreInfo.Ruleset => Ruleset; + IBeatmapInfo IScoreInfo.Beatmap => Beatmap; + IUser IScoreInfo.User => User; + IEnumerable IHasNamedFiles.Files => Files; + } +} From a5df01ff47af0d44af5852a462628e3dab998236 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Nov 2021 13:07:11 +0900 Subject: [PATCH 024/227] Add score importer --- osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs | 2 +- osu.Game/Stores/ScoreImporter.cs | 64 ++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Stores/ScoreImporter.cs diff --git a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs index 0510770d5b..33d8929008 100644 --- a/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetOnlineInfo.cs @@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps DateTimeOffset? LastUpdated { get; } /// - /// The status of this beatmap set. + /// The "ranked" status of this beatmap set. /// BeatmapOnlineStatus Status { get; } diff --git a/osu.Game/Stores/ScoreImporter.cs b/osu.Game/Stores/ScoreImporter.cs new file mode 100644 index 0000000000..b30dd88fbe --- /dev/null +++ b/osu.Game/Stores/ScoreImporter.cs @@ -0,0 +1,64 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.IO.Archives; +using osu.Game.Models; +using osu.Game.Scoring.Legacy; +using Realms; + +#nullable enable + +namespace osu.Game.Stores +{ + /// + /// Handles the storage and retrieval of Scores/WorkingScores. + /// + [ExcludeFromDynamicCompile] + public class ScoreImporter : RealmArchiveModelImporter + { + private readonly RealmRulesetStore rulesets; + private readonly BeatmapManager beatmaps; + + public override IEnumerable HandledExtensions => new[] { ".osr" }; + + protected override string[] HashableFileTypes => new[] { ".osr" }; + + public ScoreImporter(RealmRulesetStore rulesets, RealmContextFactory contextFactory, Storage storage, BeatmapManager beatmaps) + : base(storage, contextFactory) + { + this.rulesets = rulesets; + this.beatmaps = beatmaps; + } + + protected override RealmScore? CreateModel(ArchiveReader archive) + { + using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) + { + try + { + // TODO: make work. + // return new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).ScoreInfo; + return new RealmScore(); + } + catch (LegacyScoreDecoder.BeatmapNotFoundException e) + { + Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); + return null; + } + } + } + + protected override Task Populate(RealmScore model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + => Task.CompletedTask; + } +} From c5e401d6788bf7219b5118186b1c4b866c956deb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Dec 2021 17:50:40 +0900 Subject: [PATCH 025/227] Update usages to consume `IRulesetStore` --- osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs index 9b590f56dd..a49eb89ecc 100644 --- a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs @@ -12,10 +12,10 @@ namespace osu.Game.Scoring.Legacy /// public class DatabasedLegacyScoreDecoder : LegacyScoreDecoder { - private readonly RulesetStore rulesets; + private readonly IRulesetStore rulesets; private readonly BeatmapManager beatmaps; - public DatabasedLegacyScoreDecoder(RulesetStore rulesets, BeatmapManager beatmaps) + public DatabasedLegacyScoreDecoder(IRulesetStore rulesets, BeatmapManager beatmaps) { this.rulesets = rulesets; this.beatmaps = beatmaps; From 3da762e1457ac3631c1d6a4830b7c8fe8129692f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 15:31:40 +0900 Subject: [PATCH 026/227] Replace EF `ScoreInfo` with realm version May contain errors. --- osu.Game/Database/OsuDbContext.cs | 2 +- osu.Game/Models/RealmScore.cs | 68 -------- osu.Game/Scoring/EFScoreInfo.cs | 265 ++++++++++++++++++++++++++++ osu.Game/Scoring/ScoreInfo.cs | 278 +++++------------------------- osu.Game/Stores/ScoreImporter.cs | 9 +- 5 files changed, 310 insertions(+), 312 deletions(-) delete mode 100644 osu.Game/Models/RealmScore.cs create mode 100644 osu.Game/Scoring/EFScoreInfo.cs diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 9f0bae4a66..fb83592c01 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -26,7 +26,7 @@ namespace osu.Game.Database public DbSet FileInfo { get; set; } public DbSet RulesetInfo { get; set; } public DbSet SkinInfo { get; set; } - public DbSet ScoreInfo { get; set; } + public DbSet ScoreInfo { get; set; } // migrated to realm public DbSet DatabasedSetting { get; set; } diff --git a/osu.Game/Models/RealmScore.cs b/osu.Game/Models/RealmScore.cs deleted file mode 100644 index 323bd2393c..0000000000 --- a/osu.Game/Models/RealmScore.cs +++ /dev/null @@ -1,68 +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 System; -using System.Collections.Generic; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Rulesets; -using osu.Game.Scoring; -using osu.Game.Users; -using Realms; - -#nullable enable - -namespace osu.Game.Models -{ - [ExcludeFromDynamicCompile] - [MapTo("Score")] - public class RealmScore : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IScoreInfo - { - [PrimaryKey] - public Guid ID { get; set; } = Guid.NewGuid(); - - public IList Files { get; } = null!; - - public string Hash { get; set; } = string.Empty; - - public bool DeletePending { get; set; } - - public bool Equals(RealmScore other) => other.ID == ID; - - [Indexed] - public long OnlineID { get; set; } = -1; - - public RealmUser User { get; set; } = null!; - - public long TotalScore { get; set; } - - public int MaxCombo { get; set; } - - public double Accuracy { get; set; } - - public bool HasReplay { get; set; } - - public DateTimeOffset Date { get; set; } - - public double? PP { get; set; } = null; - - public RealmBeatmap Beatmap { get; set; } = null!; - - public RealmRuleset Ruleset { get; set; } = null!; - - public ScoreRank Rank - { - get => (ScoreRank)RankInt; - set => RankInt = (int)value; - } - - [MapTo(nameof(Rank))] - public int RankInt { get; set; } - - IRulesetInfo IScoreInfo.Ruleset => Ruleset; - IBeatmapInfo IScoreInfo.Beatmap => Beatmap; - IUser IScoreInfo.User => User; - IEnumerable IHasNamedFiles.Files => Files; - } -} diff --git a/osu.Game/Scoring/EFScoreInfo.cs b/osu.Game/Scoring/EFScoreInfo.cs new file mode 100644 index 0000000000..e23662f74d --- /dev/null +++ b/osu.Game/Scoring/EFScoreInfo.cs @@ -0,0 +1,265 @@ +// 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.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using Newtonsoft.Json; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Users; +using osu.Game.Utils; + +namespace osu.Game.Scoring +{ + public class EFScoreInfo : IScoreInfo, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable, IDeepCloneable + { + public int ID { get; set; } + + public ScoreRank Rank { get; set; } + + public long TotalScore { get; set; } + + [Column(TypeName = "DECIMAL(1,4)")] // TODO: This data type is wrong (should contain more precision). But at the same time, we probably don't need to be storing this in the database. + public double Accuracy { get; set; } + + public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy(); + + public double? PP { get; set; } + + public int MaxCombo { get; set; } + + public int Combo { get; set; } // Todo: Shouldn't exist in here + + public int RulesetID { get; set; } + + [NotMapped] + public bool Passed { get; set; } = true; + + public RulesetInfo Ruleset { get; set; } + + private APIMod[] localAPIMods; + + private Mod[] mods; + + [NotMapped] + public Mod[] Mods + { + get + { + var rulesetInstance = Ruleset?.CreateInstance(); + if (rulesetInstance == null) + return mods ?? Array.Empty(); + + Mod[] scoreMods = Array.Empty(); + + if (mods != null) + scoreMods = mods; + else if (localAPIMods != null) + scoreMods = APIMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + return scoreMods; + } + set + { + localAPIMods = null; + mods = value; + } + } + + // Used for API serialisation/deserialisation. + [NotMapped] + public APIMod[] APIMods + { + get + { + if (localAPIMods != null) + return localAPIMods; + + if (mods == null) + return Array.Empty(); + + return localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); + } + set + { + localAPIMods = value; + + // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. + mods = null; + } + } + + // Used for database serialisation/deserialisation. + [Column("Mods")] + public string ModsJson + { + get => JsonConvert.SerializeObject(APIMods); + set => APIMods = JsonConvert.DeserializeObject(value); + } + + [NotMapped] + public APIUser User { get; set; } + + [Column("User")] + public string UserString + { + get => User?.Username; + set + { + User ??= new APIUser(); + User.Username = value; + } + } + + [Column("UserID")] + public int? UserID + { + get => User?.Id ?? 1; + set + { + User ??= new APIUser(); + User.Id = value ?? 1; + } + } + + public int BeatmapInfoID { get; set; } + + [Column("Beatmap")] + public BeatmapInfo BeatmapInfo { get; set; } + + public long? OnlineScoreID { get; set; } + + public DateTimeOffset Date { get; set; } + + [NotMapped] + public Dictionary Statistics { get; set; } = new Dictionary(); + + [Column("Statistics")] + public string StatisticsJson + { + get => JsonConvert.SerializeObject(Statistics); + set + { + if (value == null) + { + Statistics.Clear(); + return; + } + + Statistics = JsonConvert.DeserializeObject>(value); + } + } + + [NotMapped] + public List HitEvents { get; set; } + + public List Files { get; } = new List(); + + public string Hash { get; set; } + + public bool DeletePending { get; set; } + + /// + /// The position of this score, starting at 1. + /// + [NotMapped] + public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone. + + /// + /// Whether this represents a legacy (osu!stable) score. + /// + [NotMapped] + public bool IsLegacyScore => Mods.OfType().Any(); + + public IEnumerable GetStatisticsForDisplay() + { + foreach (var r in Ruleset.CreateInstance().GetHitResults()) + { + int value = Statistics.GetValueOrDefault(r.result); + + switch (r.result) + { + case HitResult.SmallTickHit: + { + int total = value + Statistics.GetValueOrDefault(HitResult.SmallTickMiss); + if (total > 0) + yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); + + break; + } + + case HitResult.LargeTickHit: + { + int total = value + Statistics.GetValueOrDefault(HitResult.LargeTickMiss); + if (total > 0) + yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); + + break; + } + + case HitResult.SmallTickMiss: + case HitResult.LargeTickMiss: + break; + + default: + yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName); + + break; + } + } + } + + public EFScoreInfo DeepClone() + { + var clone = (EFScoreInfo)MemberwiseClone(); + + clone.Statistics = new Dictionary(clone.Statistics); + + return clone; + } + + public override string ToString() => this.GetDisplayTitle(); + + public bool Equals(EFScoreInfo other) + { + if (other == null) + return false; + + if (ID != 0 && other.ID != 0) + return ID == other.ID; + + if (OnlineScoreID.HasValue && other.OnlineScoreID.HasValue) + return OnlineScoreID == other.OnlineScoreID; + + if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) + return Hash == other.Hash; + + return ReferenceEquals(this, other); + } + + #region Implementation of IHasOnlineID + + public long OnlineID => OnlineScoreID ?? -1; + + #endregion + + #region Implementation of IScoreInfo + + IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; + IRulesetInfo IScoreInfo.Ruleset => Ruleset; + IUser IScoreInfo.User => User; + bool IScoreInfo.HasReplay => Files.Any(); + + #endregion + + IEnumerable IHasNamedFiles.Files => Files; + } +} diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 7acc7bd055..0c097d1294 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -3,266 +3,66 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using Newtonsoft.Json; -using osu.Framework.Localisation; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Models; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Users; -using osu.Game.Utils; +using Realms; + +#nullable enable namespace osu.Game.Scoring { - public class ScoreInfo : IScoreInfo, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable, IDeepCloneable + [ExcludeFromDynamicCompile] + [MapTo("Score")] + public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IScoreInfo { - public int ID { get; set; } + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); - public bool IsManaged => ID > 0; + public IList Files { get; } = null!; - public ScoreRank Rank { get; set; } - - public long TotalScore { get; set; } - - [Column(TypeName = "DECIMAL(1,4)")] // TODO: This data type is wrong (should contain more precision). But at the same time, we probably don't need to be storing this in the database. - public double Accuracy { get; set; } - - public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy(); - - public double? PP { get; set; } - - public int MaxCombo { get; set; } - - public int Combo { get; set; } // Todo: Shouldn't exist in here - - public int RulesetID { get; set; } - - [NotMapped] - public bool Passed { get; set; } = true; - - public RulesetInfo Ruleset { get; set; } - - private APIMod[] localAPIMods; - - private Mod[] mods; - - [NotMapped] - public Mod[] Mods - { - get - { - var rulesetInstance = Ruleset?.CreateInstance(); - if (rulesetInstance == null) - return mods ?? Array.Empty(); - - Mod[] scoreMods = Array.Empty(); - - if (mods != null) - scoreMods = mods; - else if (localAPIMods != null) - scoreMods = APIMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - - return scoreMods; - } - set - { - localAPIMods = null; - mods = value; - } - } - - // Used for API serialisation/deserialisation. - [NotMapped] - public APIMod[] APIMods - { - get - { - if (localAPIMods != null) - return localAPIMods; - - if (mods == null) - return Array.Empty(); - - return localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); - } - set - { - localAPIMods = value; - - // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. - mods = null; - } - } - - // Used for database serialisation/deserialisation. - [Column("Mods")] - public string ModsJson - { - get => JsonConvert.SerializeObject(APIMods); - set => APIMods = JsonConvert.DeserializeObject(value); - } - - [NotMapped] - public APIUser User { get; set; } - - [Column("User")] - public string UserString - { - get => User?.Username; - set - { - User ??= new APIUser(); - User.Username = value; - } - } - - [Column("UserID")] - public int? UserID - { - get => User?.Id ?? 1; - set - { - User ??= new APIUser(); - User.Id = value ?? 1; - } - } - - public int BeatmapInfoID { get; set; } - - [Column("Beatmap")] - public BeatmapInfo BeatmapInfo { get; set; } - - private long? onlineID; - - [Column("OnlineScoreID")] - public long? OnlineID - { - get => onlineID; - set => onlineID = value > 0 ? value : null; - } - - public DateTimeOffset Date { get; set; } - - [NotMapped] - public Dictionary Statistics { get; set; } = new Dictionary(); - - [Column("Statistics")] - public string StatisticsJson - { - get => JsonConvert.SerializeObject(Statistics); - set - { - if (value == null) - { - Statistics.Clear(); - return; - } - - Statistics = JsonConvert.DeserializeObject>(value); - } - } - - [NotMapped] - public List HitEvents { get; set; } - - public List Files { get; } = new List(); - - public string Hash { get; set; } + public string Hash { get; set; } = string.Empty; public bool DeletePending { get; set; } - /// - /// The position of this score, starting at 1. - /// - [NotMapped] - public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone. + public bool Equals(ScoreInfo other) => other.ID == ID; - /// - /// Whether this represents a legacy (osu!stable) score. - /// - [NotMapped] - public bool IsLegacyScore => Mods.OfType().Any(); + [Indexed] + public long OnlineID { get; set; } = -1; - public IEnumerable GetStatisticsForDisplay() + public RealmUser User { get; set; } = null!; + + public long TotalScore { get; set; } + + public int MaxCombo { get; set; } + + public double Accuracy { get; set; } + + public bool HasReplay { get; set; } + + public DateTimeOffset Date { get; set; } + + public double? PP { get; set; } + + public RealmBeatmap Beatmap { get; set; } = null!; + + public RealmRuleset Ruleset { get; set; } = null!; + + public ScoreRank Rank { - foreach (var r in Ruleset.CreateInstance().GetHitResults()) - { - int value = Statistics.GetValueOrDefault(r.result); - - switch (r.result) - { - case HitResult.SmallTickHit: - { - int total = value + Statistics.GetValueOrDefault(HitResult.SmallTickMiss); - if (total > 0) - yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); - - break; - } - - case HitResult.LargeTickHit: - { - int total = value + Statistics.GetValueOrDefault(HitResult.LargeTickMiss); - if (total > 0) - yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); - - break; - } - - case HitResult.SmallTickMiss: - case HitResult.LargeTickMiss: - break; - - default: - yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName); - - break; - } - } + get => (ScoreRank)RankInt; + set => RankInt = (int)value; } - public ScoreInfo DeepClone() - { - var clone = (ScoreInfo)MemberwiseClone(); + [MapTo(nameof(Rank))] + public int RankInt { get; set; } - clone.Statistics = new Dictionary(clone.Statistics); - - return clone; - } - - public override string ToString() => this.GetDisplayTitle(); - - public bool Equals(ScoreInfo other) - { - if (ReferenceEquals(this, other)) return true; - if (other == null) return false; - - if (ID != 0 && other.ID != 0) - return ID == other.ID; - - return false; - } - - #region Implementation of IHasOnlineID - - long IHasOnlineID.OnlineID => OnlineID ?? -1; - - #endregion - - #region Implementation of IScoreInfo - - IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; IRulesetInfo IScoreInfo.Ruleset => Ruleset; + IBeatmapInfo IScoreInfo.Beatmap => Beatmap; IUser IScoreInfo.User => User; - bool IScoreInfo.HasReplay => Files.Any(); - - #endregion - IEnumerable IHasNamedFiles.Files => Files; } } diff --git a/osu.Game/Stores/ScoreImporter.cs b/osu.Game/Stores/ScoreImporter.cs index b30dd88fbe..256f73d8e7 100644 --- a/osu.Game/Stores/ScoreImporter.cs +++ b/osu.Game/Stores/ScoreImporter.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Models; +using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using Realms; @@ -24,7 +25,7 @@ namespace osu.Game.Stores /// Handles the storage and retrieval of Scores/WorkingScores. /// [ExcludeFromDynamicCompile] - public class ScoreImporter : RealmArchiveModelImporter + public class ScoreImporter : RealmArchiveModelImporter { private readonly RealmRulesetStore rulesets; private readonly BeatmapManager beatmaps; @@ -40,7 +41,7 @@ namespace osu.Game.Stores this.beatmaps = beatmaps; } - protected override RealmScore? CreateModel(ArchiveReader archive) + protected override ScoreInfo? CreateModel(ArchiveReader archive) { using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) { @@ -48,7 +49,7 @@ namespace osu.Game.Stores { // TODO: make work. // return new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).ScoreInfo; - return new RealmScore(); + return new ScoreInfo(); } catch (LegacyScoreDecoder.BeatmapNotFoundException e) { @@ -58,7 +59,7 @@ namespace osu.Game.Stores } } - protected override Task Populate(RealmScore model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + protected override Task Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) => Task.CompletedTask; } } From 638b3d91610dd3cf548f859ee704f9963aa5bc3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 15:32:02 +0900 Subject: [PATCH 027/227] Add statistics storage to realm model --- osu.Game/Scoring/ScoreInfo.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 0c097d1294..1938271989 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -51,6 +51,22 @@ namespace osu.Game.Scoring public RealmRuleset Ruleset { get; set; } = null!; + [Ignored] + public Dictionary Statistics + { + get + { + if (string.IsNullOrEmpty(StatisticsJson)) + return new Dictionary(); + + return JsonConvert.DeserializeObject>(StatisticsJson) ?? new Dictionary(); + } + set => JsonConvert.SerializeObject(StatisticsJson); + } + + [MapTo("Statistics")] + public string StatisticsJson { get; set; } = null!; + public ScoreRank Rank { get => (ScoreRank)RankInt; From e44751c275918bcf19da500e40dd57e7c1672591 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 15:35:08 +0900 Subject: [PATCH 028/227] Add required properties for compatibility with existing code --- osu.Game/Scoring/ScoreInfo.cs | 133 ++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 1938271989..11cd26dd3c 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -3,11 +3,17 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Users; using Realms; @@ -80,5 +86,132 @@ namespace osu.Game.Scoring IBeatmapInfo IScoreInfo.Beatmap => Beatmap; IUser IScoreInfo.User => User; IEnumerable IHasNamedFiles.Files => Files; + + #region Properties required to make things work with existing usages + + private APIMod[]? localAPIMods; + + private Mod[]? mods; + + [Ignored] + public List HitEvents { get; set; } = new List(); + + public ScoreInfo DeepClone() + { + var clone = (ScoreInfo)MemberwiseClone(); + + clone.Statistics = new Dictionary(clone.Statistics); + + return clone; + } + + [Ignored] + public bool Passed { get; set; } = true; + + /// + /// The position of this score, starting at 1. + /// + [Ignored] + public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone. + + /// + /// Whether this represents a legacy (osu!stable) score. + /// + [Ignored] + public bool IsLegacyScore => Mods.OfType().Any(); + + [Ignored] + public Mod[] Mods + { + get + { + var rulesetInstance = Ruleset.CreateInstance(); + + Mod[] scoreMods = Array.Empty(); + + if (mods != null) + scoreMods = mods; + else if (localAPIMods != null) + scoreMods = APIMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + return scoreMods; + } + set + { + localAPIMods = null; + mods = value; + } + } + + // Used for API serialisation/deserialisation. + [Ignored] + public APIMod[] APIMods + { + get + { + if (localAPIMods != null) + return localAPIMods; + + if (mods == null) + return Array.Empty(); + + return localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); + } + set + { + localAPIMods = value; + + // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. + mods = null; + } + } + + // Used for database serialisation/deserialisation. + [Column("Mods")] + public string ModsJson + { + get => JsonConvert.SerializeObject(APIMods); + set => APIMods = JsonConvert.DeserializeObject(value) ?? Array.Empty(); + } + + public IEnumerable GetStatisticsForDisplay() + { + foreach (var r in Ruleset.CreateInstance().GetHitResults()) + { + int value = Statistics.GetValueOrDefault(r.result); + + switch (r.result) + { + case HitResult.SmallTickHit: + { + int total = value + Statistics.GetValueOrDefault(HitResult.SmallTickMiss); + if (total > 0) + yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); + + break; + } + + case HitResult.LargeTickHit: + { + int total = value + Statistics.GetValueOrDefault(HitResult.LargeTickMiss); + if (total > 0) + yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); + + break; + } + + case HitResult.SmallTickMiss: + case HitResult.LargeTickMiss: + break; + + default: + yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName); + + break; + } + } + } + + #endregion } } From 2a4bee61dde38d203dcaeca6da89fe904ab2e80d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Dec 2021 22:47:00 +0900 Subject: [PATCH 029/227] Update many score-related classes to move closer to being able to persist to realm --- osu.Game/Database/LegacyScoreExporter.cs | 2 +- .../Online/API/Requests/Responses/APIScore.cs | 1 - osu.Game/Online/Rooms/MultiplayerScore.cs | 2 -- osu.Game/Online/Solo/SubmittableScore.cs | 3 +- osu.Game/OsuGame.cs | 2 +- osu.Game/Scoring/EFScoreInfo.cs | 2 ++ osu.Game/Scoring/LegacyDatabasedScore.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 36 ++++++++++++++++++- osu.Game/Scoring/ScoreManager.cs | 10 +++--- osu.Game/Scoring/ScoreModelManager.cs | 29 +++++---------- osu.Game/Scoring/ScoreStore.cs | 25 ------------- osu.Game/Screens/Ranking/ScorePanelList.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- osu.Game/Stores/ScoreImporter.cs | 1 - 14 files changed, 58 insertions(+), 61 deletions(-) delete mode 100644 osu.Game/Scoring/ScoreStore.cs diff --git a/osu.Game/Database/LegacyScoreExporter.cs b/osu.Game/Database/LegacyScoreExporter.cs index 41f8516880..336f50bc3d 100644 --- a/osu.Game/Database/LegacyScoreExporter.cs +++ b/osu.Game/Database/LegacyScoreExporter.cs @@ -24,7 +24,7 @@ namespace osu.Game.Database if (file == null) return; - using (var inputStream = UserFileStorage.GetStream(file.FileInfo.GetStoragePath())) + using (var inputStream = UserFileStorage.GetStream(file.File.GetStoragePath())) inputStream.CopyTo(outputStream); } } diff --git a/osu.Game/Online/API/Requests/Responses/APIScore.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs index 4f795bee6c..71f277570d 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScore.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs @@ -105,7 +105,6 @@ namespace osu.Game.Online.API.Requests.Responses OnlineID = OnlineID, Date = Date, PP = PP, - RulesetID = RulesetID, Hash = HasReplay ? "online" : string.Empty, // todo: temporary? Rank = Rank, Ruleset = ruleset, diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 05c9a1b6cf..b90680a925 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -73,9 +73,7 @@ namespace osu.Game.Online.Rooms TotalScore = TotalScore, MaxCombo = MaxCombo, BeatmapInfo = beatmap, - BeatmapInfoID = playlistItem.BeatmapID, Ruleset = rulesets.GetRuleset(playlistItem.RulesetID), - RulesetID = playlistItem.RulesetID, Statistics = Statistics, User = User, Accuracy = Accuracy, diff --git a/osu.Game/Online/Solo/SubmittableScore.cs b/osu.Game/Online/Solo/SubmittableScore.cs index 5ca5ad9619..52aef82c3b 100644 --- a/osu.Game/Online/Solo/SubmittableScore.cs +++ b/osu.Game/Online/Solo/SubmittableScore.cs @@ -10,6 +10,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Users; using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Online.Solo @@ -48,7 +49,7 @@ namespace osu.Game.Online.Solo public APIMod[] Mods { get; set; } [JsonProperty("user")] - public APIUser User { get; set; } + public IUser User { get; set; } [JsonProperty("statistics")] public Dictionary Statistics { get; set; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0d0a572663..544e8df425 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -481,7 +481,7 @@ namespace osu.Game /// Present a score's replay immediately. /// The user should have already requested this interactively. /// - public void PresentScore(ScoreInfo score, ScorePresentType presentType = ScorePresentType.Results) + public void PresentScore(IScoreInfo score, ScorePresentType presentType = ScorePresentType.Results) { // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database // to ensure all the required data for presenting a replay are present. diff --git a/osu.Game/Scoring/EFScoreInfo.cs b/osu.Game/Scoring/EFScoreInfo.cs index e23662f74d..84b41e40ef 100644 --- a/osu.Game/Scoring/EFScoreInfo.cs +++ b/osu.Game/Scoring/EFScoreInfo.cs @@ -23,6 +23,8 @@ namespace osu.Game.Scoring { public int ID { get; set; } + public bool IsManaged => ID > 0; + public ScoreRank Rank { get; set; } public long TotalScore { get; set; } diff --git a/osu.Game/Scoring/LegacyDatabasedScore.cs b/osu.Game/Scoring/LegacyDatabasedScore.cs index 69360cacc7..ac444c1bf3 100644 --- a/osu.Game/Scoring/LegacyDatabasedScore.cs +++ b/osu.Game/Scoring/LegacyDatabasedScore.cs @@ -17,7 +17,7 @@ namespace osu.Game.Scoring { ScoreInfo = score; - string replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(".osr", StringComparison.InvariantCultureIgnoreCase))?.FileInfo.GetStoragePath(); + string replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath(); if (replayFilename == null) return; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 11cd26dd3c..782fe0681b 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; @@ -15,6 +16,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Users; +using osu.Game.Utils; using Realms; #nullable enable @@ -39,7 +41,18 @@ namespace osu.Game.Scoring [Indexed] public long OnlineID { get; set; } = -1; - public RealmUser User { get; set; } = null!; + [MapTo("User")] + public RealmUser RealmUser { get; set; } = null!; + + public IUser User + { + get => RealmUser; + set => RealmUser = new RealmUser + { + OnlineID = value.OnlineID, + Username = value.Username + }; + } public long TotalScore { get; set; } @@ -55,11 +68,20 @@ namespace osu.Game.Scoring public RealmBeatmap Beatmap { get; set; } = null!; + public BeatmapInfo BeatmapInfo + { + get => new BeatmapInfo(); + // .. todo + set => Beatmap = new RealmBeatmap(new RealmRuleset("osu", "osu!", "wangs", 0), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + } + public RealmRuleset Ruleset { get; set; } = null!; [Ignored] public Dictionary Statistics { + // TODO: this is dangerous. a get operation may then modify the dictionary, which would be a fresh copy that is not persisted with the model. + // this is already the case in multiple locations. get { if (string.IsNullOrEmpty(StatisticsJson)) @@ -93,6 +115,12 @@ namespace osu.Game.Scoring private Mod[]? mods; + public int BeatmapInfoID => BeatmapInfo.ID; + + public int UserID => RealmUser.OnlineID; + + public int RulesetID => Ruleset.OnlineID; + [Ignored] public List HitEvents { get; set; } = new List(); @@ -108,12 +136,18 @@ namespace osu.Game.Scoring [Ignored] public bool Passed { get; set; } = true; + [Ignored] + public int Combo { get; set; } + /// /// The position of this score, starting at 1. /// [Ignored] public int? Position { get; set; } // TODO: remove after all calls to `CreateScoreInfo` are gone. + [Ignored] + public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy(); + /// /// Whether this represents a legacy (osu!stable) score. /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 9f248ffdca..740c46387e 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -63,7 +63,7 @@ namespace osu.Game.Scoring // Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below. foreach (var s in scores) { - await difficultyCache.GetDifficultyAsync(s.BeatmapInfo, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); + await difficultyCache.GetDifficultyAsync(s.Beatmap, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } } @@ -125,7 +125,7 @@ namespace osu.Game.Scoring /// The total score. public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { - if (score.BeatmapInfo == null) + if (score.Beatmap == null) return score.TotalScore; int beatmapMaxCombo; @@ -146,11 +146,11 @@ namespace osu.Game.Scoring // This score is guaranteed to be an osu!stable score. // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. - if (score.BeatmapInfo.MaxCombo != null) - beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value; + if (score.Beatmap.MaxCombo != null) + beatmapMaxCombo = score.Beatmap.MaxCombo.Value; else { - if (score.BeatmapInfo.ID == 0 || difficulties == null) + if (score.Beatmap.ID == 0 || difficulties == null) { // We don't have enough information (max combo) to compute the score, so use the provided score. return score.TotalScore; diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 44f0fe4fdf..81f80df3ad 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -4,10 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -15,10 +13,14 @@ using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Rulesets; using osu.Game.Scoring.Legacy; +using osu.Game.Stores; +using Realms; + +#nullable enable namespace osu.Game.Scoring { - public class ScoreModelManager : ArchiveModelManager + public class ScoreModelManager : RealmArchiveModelManager { public override IEnumerable HandledExtensions => new[] { ".osr" }; @@ -27,18 +29,15 @@ namespace osu.Game.Scoring private readonly RulesetStore rulesets; private readonly Func beatmaps; - public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, IDatabaseContextFactory contextFactory, IIpcHost importHost = null) - : base(storage, contextFactory, new ScoreStore(contextFactory, storage), importHost) + public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory) + : base(storage, contextFactory) { this.rulesets = rulesets; this.beatmaps = beatmaps; } - protected override ScoreInfo CreateModel(ArchiveReader archive) + protected override ScoreInfo? CreateModel(ArchiveReader archive) { - if (archive == null) - return null; - using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) { try @@ -55,17 +54,7 @@ namespace osu.Game.Scoring public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); - public List GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); - - public IEnumerable QueryScores(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); - - public ScoreInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); - - protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + protected override Task Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) => Task.CompletedTask; - - protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineID > 0 && items.Any(i => i.OnlineID == model.OnlineID)); } } diff --git a/osu.Game/Scoring/ScoreStore.cs b/osu.Game/Scoring/ScoreStore.cs deleted file mode 100644 index fd1f5ae3ec..0000000000 --- a/osu.Game/Scoring/ScoreStore.cs +++ /dev/null @@ -1,25 +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 System.Linq; -using Microsoft.EntityFrameworkCore; -using osu.Framework.Platform; -using osu.Game.Database; - -namespace osu.Game.Scoring -{ - public class ScoreStore : MutableDatabaseBackedStoreWithFileIncludes - { - public ScoreStore(IDatabaseContextFactory factory, Storage storage) - : base(factory, storage) - { - } - - protected override IQueryable AddIncludesForConsumption(IQueryable query) - => base.AddIncludesForConsumption(query) - .Include(s => s.BeatmapInfo) - .Include(s => s.BeatmapInfo).ThenInclude(b => b.Metadata) - .Include(s => s.BeatmapInfo).ThenInclude(b => b.BeatmapSet).ThenInclude(s => s.Metadata) - .Include(s => s.Ruleset); - } -} diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index f8e9d08350..eb91c256c7 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -196,7 +196,7 @@ namespace osu.Game.Screens.Ranking } // Find the panel corresponding to the new score. - var expandedTrackingComponent = flow.SingleOrDefault(t => t.Panel.Score == score.NewValue); + var expandedTrackingComponent = flow.SingleOrDefault(t => t.Panel.Score.Equals(score.NewValue)); expandedPanel = expandedTrackingComponent?.Panel; if (expandedPanel == null) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 3b25d34aec..7026f240c8 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { var scores = scoreManager - .QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID); + .QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { diff --git a/osu.Game/Stores/ScoreImporter.cs b/osu.Game/Stores/ScoreImporter.cs index 256f73d8e7..8b43f618cc 100644 --- a/osu.Game/Stores/ScoreImporter.cs +++ b/osu.Game/Stores/ScoreImporter.cs @@ -12,7 +12,6 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; -using osu.Game.Models; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using Realms; From aac2aa341cf879c66e7b802d6d9a2ce1d1c3e890 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 17:09:13 +0900 Subject: [PATCH 030/227] Update some more incorrect types for primary key access/set --- osu.Game.Tests/Scores/IO/TestScoreEquality.cs | 11 +++++++---- osu.Game/Online/Solo/SubmittableScore.cs | 1 - osu.Game/OsuGame.cs | 3 ++- osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs | 16 ++++++++-------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs index 42fcb3acab..f898774ce6 100644 --- a/osu.Game.Tests/Scores/IO/TestScoreEquality.cs +++ b/osu.Game.Tests/Scores/IO/TestScoreEquality.cs @@ -1,6 +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; using NUnit.Framework; using osu.Game.Scoring; @@ -29,8 +30,8 @@ namespace osu.Game.Tests.Scores.IO [Test] public void TestNonMatchingByPrimaryKey() { - ScoreInfo score1 = new ScoreInfo { ID = 1 }; - ScoreInfo score2 = new ScoreInfo { ID = 2 }; + ScoreInfo score1 = new ScoreInfo { ID = Guid.NewGuid() }; + ScoreInfo score2 = new ScoreInfo { ID = Guid.NewGuid() }; Assert.That(score1, Is.Not.EqualTo(score2)); } @@ -38,8 +39,10 @@ namespace osu.Game.Tests.Scores.IO [Test] public void TestMatchingByPrimaryKey() { - ScoreInfo score1 = new ScoreInfo { ID = 1 }; - ScoreInfo score2 = new ScoreInfo { ID = 1 }; + Guid id = Guid.NewGuid(); + + ScoreInfo score1 = new ScoreInfo { ID = id }; + ScoreInfo score2 = new ScoreInfo { ID = id }; Assert.That(score1, Is.EqualTo(score2)); } diff --git a/osu.Game/Online/Solo/SubmittableScore.cs b/osu.Game/Online/Solo/SubmittableScore.cs index 52aef82c3b..327806f390 100644 --- a/osu.Game/Online/Solo/SubmittableScore.cs +++ b/osu.Game/Online/Solo/SubmittableScore.cs @@ -11,7 +11,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Users; -using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Online.Solo { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 544e8df425..4539409d4c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -490,7 +490,8 @@ namespace osu.Game if (score.OnlineID > 0) databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID); - databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash); + if (score is ScoreInfo scoreInfo) + databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash); if (databasedScoreInfo == null) { diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index fc27261225..a11cd5fcbd 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -9,7 +9,7 @@ namespace osu.Game.Scoring.Legacy { public static int? GetCountGeki(this ScoreInfo scoreInfo) { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) + switch (scoreInfo.Ruleset.OnlineID) { case 3: return getCount(scoreInfo, HitResult.Perfect); @@ -20,7 +20,7 @@ namespace osu.Game.Scoring.Legacy public static void SetCountGeki(this ScoreInfo scoreInfo, int value) { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) + switch (scoreInfo.Ruleset.OnlineID) { case 3: scoreInfo.Statistics[HitResult.Perfect] = value; @@ -34,7 +34,7 @@ namespace osu.Game.Scoring.Legacy public static int? GetCountKatu(this ScoreInfo scoreInfo) { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) + switch (scoreInfo.Ruleset.OnlineID) { case 3: return getCount(scoreInfo, HitResult.Good); @@ -48,7 +48,7 @@ namespace osu.Game.Scoring.Legacy public static void SetCountKatu(this ScoreInfo scoreInfo, int value) { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) + switch (scoreInfo.Ruleset.OnlineID) { case 3: scoreInfo.Statistics[HitResult.Good] = value; @@ -62,7 +62,7 @@ namespace osu.Game.Scoring.Legacy public static int? GetCount100(this ScoreInfo scoreInfo) { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) + switch (scoreInfo.Ruleset.OnlineID) { case 0: case 1: @@ -78,7 +78,7 @@ namespace osu.Game.Scoring.Legacy public static void SetCount100(this ScoreInfo scoreInfo, int value) { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) + switch (scoreInfo.Ruleset.OnlineID) { case 0: case 1: @@ -94,7 +94,7 @@ namespace osu.Game.Scoring.Legacy public static int? GetCount50(this ScoreInfo scoreInfo) { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) + switch (scoreInfo.Ruleset.OnlineID) { case 0: case 3: @@ -109,7 +109,7 @@ namespace osu.Game.Scoring.Legacy public static void SetCount50(this ScoreInfo scoreInfo, int value) { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) + switch (scoreInfo.Ruleset.OnlineID) { case 0: case 3: From e711a6d35598643c9a11ed6d895db09c4abd74f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 17:37:17 +0900 Subject: [PATCH 031/227] Remove unused `ScoreImporter` class --- osu.Game/Stores/ScoreImporter.cs | 64 -------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 osu.Game/Stores/ScoreImporter.cs diff --git a/osu.Game/Stores/ScoreImporter.cs b/osu.Game/Stores/ScoreImporter.cs deleted file mode 100644 index 8b43f618cc..0000000000 --- a/osu.Game/Stores/ScoreImporter.cs +++ /dev/null @@ -1,64 +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 System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.IO.Archives; -using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; -using Realms; - -#nullable enable - -namespace osu.Game.Stores -{ - /// - /// Handles the storage and retrieval of Scores/WorkingScores. - /// - [ExcludeFromDynamicCompile] - public class ScoreImporter : RealmArchiveModelImporter - { - private readonly RealmRulesetStore rulesets; - private readonly BeatmapManager beatmaps; - - public override IEnumerable HandledExtensions => new[] { ".osr" }; - - protected override string[] HashableFileTypes => new[] { ".osr" }; - - public ScoreImporter(RealmRulesetStore rulesets, RealmContextFactory contextFactory, Storage storage, BeatmapManager beatmaps) - : base(storage, contextFactory) - { - this.rulesets = rulesets; - this.beatmaps = beatmaps; - } - - protected override ScoreInfo? CreateModel(ArchiveReader archive) - { - using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) - { - try - { - // TODO: make work. - // return new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).ScoreInfo; - return new ScoreInfo(); - } - catch (LegacyScoreDecoder.BeatmapNotFoundException e) - { - Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); - return null; - } - } - } - - protected override Task Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) - => Task.CompletedTask; - } -} From 53792811b2648611844650934d8d24f265890597 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 17:37:27 +0900 Subject: [PATCH 032/227] more fixes (almost compiles, except ruleset and manager) --- .../Beatmaps/IO/ImportBeatmapTest.cs | 11 ++++++++ osu.Game.Tests/Resources/TestResources.cs | 1 - .../Visual/Ranking/TestSceneScorePanelList.cs | 11 ++++---- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 4 ++- .../TestSceneDeleteLocalScore.cs | 3 ++- .../Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- .../Scores/TopScoreStatisticsSection.cs | 4 +-- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 4 +-- osu.Game/Scoring/LegacyDatabasedScore.cs | 4 +-- osu.Game/Scoring/ScoreInfo.cs | 27 +++++++++++++++---- osu.Game/Scoring/ScoreManager.cs | 10 ++++--- osu.Game/Scoring/ScoreModelManager.cs | 5 ++-- osu.Game/Screens/Play/Player.cs | 5 ++-- .../ContractedPanelMiddleContent.cs | 2 +- osu.Game/Screens/Ranking/ScorePanelList.cs | 14 +++++----- .../Ranking/Statistics/StatisticsPanel.cs | 2 +- 17 files changed, 71 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index da414dcf8d..0d973c8aeb 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -12,7 +12,9 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Scoring; using osu.Game.Tests.Resources; +using osu.Game.Tests.Scores.IO; namespace osu.Game.Tests.Beatmaps.IO { @@ -61,6 +63,15 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } + private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo) + { + return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo + { + OnlineID = 2, + BeatmapInfo = beatmapInfo, + }, new ImportScoreTest.TestArchiveReader()); + } + private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) { var manager = osu.Dependencies.Get(); diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index cd8b031f64..d0ffb9c3db 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -164,7 +164,6 @@ namespace osu.Game.Tests.Resources }, BeatmapInfo = beatmap, Ruleset = beatmap.Ruleset, - RulesetID = beatmap.Ruleset.ID ?? 0, Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() }, TotalScore = 2845370, Accuracy = 0.95, diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index f5ad352b9c..e786b85f78 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Models; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Tests.Resources; @@ -157,10 +158,10 @@ namespace osu.Game.Tests.Visual.Ranking public void TestSelectMultipleScores() { var firstScore = TestResources.CreateTestScoreInfo(); - var secondScore = TestResources.CreateTestScoreInfo(); + firstScore.RealmUser = new RealmUser { Username = "A" }; - firstScore.UserString = "A"; - secondScore.UserString = "B"; + var secondScore = TestResources.CreateTestScoreInfo(); + secondScore.RealmUser = new RealmUser { Username = "B" }; createListStep(() => new ScorePanelList()); @@ -178,7 +179,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("select second score", () => { - InputManager.MoveMouseTo(list.ChildrenOfType().Single(p => p.Score == secondScore)); + InputManager.MoveMouseTo(list.ChildrenOfType().Single(p => p.Score.Equals(secondScore))); InputManager.Click(MouseButton.Left); }); @@ -303,6 +304,6 @@ namespace osu.Game.Tests.Visual.Ranking => AddUntilStep("first panel centred", () => Precision.AlmostEquals(list.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1)); private void assertScoreState(ScoreInfo score, bool expanded) - => AddUntilStep($"score expanded = {expanded}", () => (list.ChildrenOfType().Single(p => p.Score == score).State == PanelState.Expanded) == expanded); + => AddUntilStep($"score expanded = {expanded}", () => (list.ChildrenOfType().Single(p => p.Score.Equals(score)).State == PanelState.Expanded) == expanded); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 605e03564d..566ee52247 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -19,6 +20,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Stores; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -43,7 +45,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, ContextFactory, Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); return dependencies; } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index cdb81f059b..1c75ef1895 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -24,6 +24,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Stores; using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; @@ -85,7 +86,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, ContextFactory, Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); beatmapInfo = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely().Value.Beatmaps[0]; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 14eec8b388..3eb9a02a5c 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -399,7 +399,7 @@ namespace osu.Game.Online.Leaderboards if (Score.Files.Count > 0) items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score))); - if (Score.ID != 0) + if (Score.IsManaged) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); return items.ToArray(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index c4dcc35b8e..89e3fe5742 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -224,7 +224,7 @@ namespace osu.Game dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, 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, realmFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realmFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realmFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 630aa8fe53..d659df21c6 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -107,7 +107,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { set { - if (score == value) + if (score.Equals(value)) return; score = value; @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); - ppColumn.Alpha = value.BeatmapInfo?.Status.GrantsPerformancePoints() == true ? 1 : 0; + ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0; ppColumn.Text = value.PP?.ToLocalisableString(@"N0"); statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 3d67aa9558..2902ff7848 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -45,8 +45,8 @@ namespace osu.Game.Scoring.Legacy sw.Write((byte)(score.ScoreInfo.Ruleset.OnlineID)); sw.Write(LATEST_VERSION); sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); - sw.Write(score.ScoreInfo.UserString); - sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}").ComputeMD5Hash()); + sw.Write(score.ScoreInfo.User.Username); + sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.User.Username}-{score.ScoreInfo.Date}").ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); sw.Write((ushort)(score.ScoreInfo.GetCount100() ?? 0)); sw.Write((ushort)(score.ScoreInfo.GetCount50() ?? 0)); diff --git a/osu.Game/Scoring/LegacyDatabasedScore.cs b/osu.Game/Scoring/LegacyDatabasedScore.cs index ac444c1bf3..6a8e5f8930 100644 --- a/osu.Game/Scoring/LegacyDatabasedScore.cs +++ b/osu.Game/Scoring/LegacyDatabasedScore.cs @@ -6,14 +6,14 @@ using System.Linq; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Extensions; -using osu.Game.Rulesets; using osu.Game.Scoring.Legacy; +using osu.Game.Stores; namespace osu.Game.Scoring { public class LegacyDatabasedScore : Score { - public LegacyDatabasedScore(ScoreInfo score, RulesetStore rulesets, BeatmapManager beatmaps, IResourceStore store) + public LegacyDatabasedScore(ScoreInfo score, RealmRulesetStore rulesets, BeatmapManager beatmaps, IResourceStore store) { ScoreInfo = score; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 782fe0681b..21339acd22 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Models; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -44,14 +45,27 @@ namespace osu.Game.Scoring [MapTo("User")] public RealmUser RealmUser { get; set; } = null!; - public IUser User + // TODO: this is a bit temporary to account for the fact that this class is used to ferry API user data to certain UI components. + // Eventually we should either persist enough information to realm to not require the API lookups, or perform the API lookups locally. + private APIUser? user; + + public APIUser User { - get => RealmUser; - set => RealmUser = new RealmUser + get => user ??= new APIUser { - OnlineID = value.OnlineID, - Username = value.Username + Username = RealmUser.Username, + Id = RealmUser.OnlineID, }; + set + { + user = value; + + RealmUser = new RealmUser + { + OnlineID = user.OnlineID, + Username = user.Username + }; + } } public long TotalScore { get; set; } @@ -72,6 +86,7 @@ namespace osu.Game.Scoring { get => new BeatmapInfo(); // .. todo + // ReSharper disable once ValueParameterNotUsed set => Beatmap = new RealmBeatmap(new RealmRuleset("osu", "osu!", "wangs", 0), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); } @@ -89,6 +104,8 @@ namespace osu.Game.Scoring return JsonConvert.DeserializeObject>(StatisticsJson) ?? new Dictionary(); } + // .. todo + // ReSharper disable once ValueParameterNotUsed set => JsonConvert.SerializeObject(StatisticsJson); } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 740c46387e..7adc70dab0 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -20,6 +20,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Stores; namespace osu.Game.Scoring { @@ -37,7 +38,7 @@ namespace osu.Game.Scoring this.difficulties = difficulties; this.configManager = configManager; - scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory, importHost); + scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory); } public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score); @@ -125,8 +126,9 @@ namespace osu.Game.Scoring /// The total score. public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { - if (score.Beatmap == null) - return score.TotalScore; + // TODO: ?? + // if (score.Beatmap == null) + // return score.TotalScore; int beatmapMaxCombo; double accuracy = score.Accuracy; @@ -150,7 +152,7 @@ namespace osu.Game.Scoring beatmapMaxCombo = score.Beatmap.MaxCombo.Value; else { - if (score.Beatmap.ID == 0 || difficulties == null) + if (!score.Beatmap.IsManaged || difficulties == null) { // We don't have enough information (max combo) to compute the score, so use the provided score. return score.TotalScore; diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 81f80df3ad..dd0ac79873 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -11,7 +11,6 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; -using osu.Game.Rulesets; using osu.Game.Scoring.Legacy; using osu.Game.Stores; using Realms; @@ -26,10 +25,10 @@ namespace osu.Game.Scoring protected override string[] HashableFileTypes => new[] { ".osr" }; - private readonly RulesetStore rulesets; + private readonly RealmRulesetStore rulesets; private readonly Func beatmaps; - public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory) + public ScoreModelManager(RealmRulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory) : base(storage, contextFactory) { this.rulesets = rulesets; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a66026b423..5a443ec48e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -226,8 +226,6 @@ namespace osu.Game.Screens.Play // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; - if (ruleset.RulesetInfo.OnlineID >= 0) - Score.ScoreInfo.RulesetID = ruleset.RulesetInfo.OnlineID; Score.ScoreInfo.Mods = gameplayMods; dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); @@ -1045,7 +1043,8 @@ namespace osu.Game.Screens.Play // // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint // conflicts across various systems (ie. solo and multiplayer). - long? onlineScoreId = score.ScoreInfo.OnlineID; + long onlineScoreId = score.ScoreInfo.OnlineID; + score.ScoreInfo.OnlineID = -1; await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false); diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 20c603295b..f9aff28bef 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Ranking.Contracted { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = score.UserString, + Text = score.RealmUser.Username, Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold) }, new FillFlowContainer diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index eb91c256c7..7f270b3a2a 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Ranking trackingContainer.Show(); - if (SelectedScore.Value == score) + if (SelectedScore.Value.Equals(score)) { SelectedScore.TriggerChange(); } @@ -185,10 +185,10 @@ namespace osu.Game.Screens.Ranking private void selectedScoreChanged(ValueChangedEvent score) { // avoid contracting panels unnecessarily when TriggerChange is fired manually. - if (score.OldValue != score.NewValue) + if (!score.OldValue.Equals(score.NewValue)) { // Contract the old panel. - foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue)) + foreach (var t in flow.Where(t => t.Panel.Score.Equals(score.OldValue))) { t.Panel.State = PanelState.Contracted; t.Margin = new MarginPadding(); @@ -269,7 +269,7 @@ namespace osu.Game.Screens.Ranking /// /// The to find the corresponding for. /// The . - public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score == score).Panel; + public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score.Equals(score)).Panel; /// /// Detaches a from its , allowing the panel to be moved elsewhere in the hierarchy. @@ -332,13 +332,13 @@ namespace osu.Game.Screens.Ranking { public override IEnumerable FlowingChildren => applySorting(AliveInternalChildren); - public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count(); + public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => !s.Panel.Score.Equals(score)).Count(); [CanBeNull] - public ScoreInfo GetPreviousScore(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).LastOrDefault()?.Panel.Score; + public ScoreInfo GetPreviousScore(ScoreInfo score) => applySorting(Children).TakeWhile(s => !s.Panel.Score.Equals(score)).LastOrDefault()?.Panel.Score; [CanBeNull] - public ScoreInfo GetNextScore(ScoreInfo score) => applySorting(Children).SkipWhile(s => s.Panel.Score != score).ElementAtOrDefault(1)?.Panel.Score; + public ScoreInfo GetNextScore(ScoreInfo score) => applySorting(Children).SkipWhile(s => !s.Panel.Score.Equals(score)).ElementAtOrDefault(1)?.Panel.Score; private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() .OrderByDescending(GetLayoutPosition) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 26dc3165f8..827e128467 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Ranking.Statistics LoadComponentAsync(rows, d => { - if (Score.Value != newScore) + if (!Score.Value.Equals(newScore)) return; spinner.Hide(); From 4f6a05ce3dff9a71275555b188e0b48eb664f149 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Dec 2021 19:01:20 +0900 Subject: [PATCH 033/227] Reimplement all query methods --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 4 +- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 10 ++++- osu.Game/OsuGame.cs | 4 +- .../Sections/Maintenance/GeneralSettings.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 31 +++++++++++--- .../Select/BeatmapClearScoresDialog.cs | 9 ++--- .../Screens/Select/Carousel/TopLocalRank.cs | 20 +++++++--- .../Select/Leaderboards/BeatmapLeaderboard.cs | 40 +++++++++++-------- 9 files changed, 81 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index bbc92b7817..f00fb97dfa 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -142,7 +142,7 @@ namespace osu.Game.Tests.Scores.IO var scoreManager = osu.Dependencies.Get(); beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))); - Assert.That(scoreManager.Query(s => s.Equals(imported)).DeletePending, Is.EqualTo(true)); + Assert.That(scoreManager.Query(s => s.Equals(imported)).Value.DeletePending, Is.EqualTo(true)); var secondImport = await LoadScoreIntoOsu(osu, imported); Assert.That(secondImport, Is.Null); @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Scores.IO var scoreManager = osu.Dependencies.Get(); await scoreManager.Import(score, archive); - return scoreManager.GetAllUsableScores().FirstOrDefault(); + return scoreManager.Query(_ => true).Value; } internal class TestArchiveReader : ArchiveReader diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 566ee52247..62b0f10fd8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -184,7 +184,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void clearScores() { - AddStep("Clear all scores", () => scoreManager.Delete(scoreManager.GetAllUsableScores())); + AddStep("Clear all scores", () => scoreManager.Delete()); } private void checkCount(int expected) => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 1c75ef1895..5c4759a466 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -44,6 +44,9 @@ namespace osu.Game.Tests.Visual.UserInterface private BeatmapInfo beatmapInfo; + [Resolved] + private RealmContextFactory realmFactory { get; set; } + [Cached] private readonly DialogOverlay dialogOverlay; @@ -112,8 +115,11 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void Setup() => Schedule(() => { - // Due to soft deletions, we can re-use deleted scores between test runs - scoreManager.Undelete(scoreManager.QueryScores(s => s.DeletePending).ToList()); + using (var realm = realmFactory.CreateContext()) + { + // Due to soft deletions, we can re-use deleted scores between test runs + scoreManager.Undelete(realm.All().Where(s => s.DeletePending).ToList()); + } leaderboard.Scores = null; leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4539409d4c..49893a6d36 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -488,10 +488,10 @@ namespace osu.Game ScoreInfo databasedScoreInfo = null; if (score.OnlineID > 0) - databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID); + databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID)?.Value; if (score is ScoreInfo scoreInfo) - databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash); + databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash)?.Value; if (databasedScoreInfo == null) { diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 98ccbf85fd..51e4d61d2e 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => { deleteScoresButton.Enabled.Value = false; - Task.Run(() => scores.Delete(scores.GetAllUsableScores())).ContinueWith(t => Schedule(() => deleteScoresButton.Enabled.Value = true)); + Task.Run(() => scores.Delete()).ContinueWith(t => Schedule(() => deleteScoresButton.Enabled.Value = true)); })); } }); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 7adc70dab0..d094e1f45c 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -26,6 +26,7 @@ namespace osu.Game.Scoring { public class ScoreManager : IModelManager, IModelImporter { + private readonly RealmContextFactory contextFactory; private readonly Scheduler scheduler; private readonly Func difficulties; private readonly OsuConfigManager configManager; @@ -34,6 +35,7 @@ namespace osu.Game.Scoring public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory, Scheduler scheduler, IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) { + this.contextFactory = contextFactory; this.scheduler = scheduler; this.difficulties = difficulties; this.configManager = configManager; @@ -43,11 +45,16 @@ namespace osu.Game.Scoring public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score); - public List GetAllUsableScores() => scoreModelManager.GetAllUsableScores(); - - public IEnumerable QueryScores(Expression> query) => scoreModelManager.QueryScores(query); - - public ScoreInfo Query(Expression> query) => scoreModelManager.Query(query); + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public ILive Query(Expression> query) + { + using (var context = contextFactory.CreateContext()) + return context.All().FirstOrDefault(query)?.ToLive(); + } /// /// Orders an array of s by total score. @@ -267,6 +274,20 @@ namespace osu.Game.Scoring return scoreModelManager.Delete(item); } + public void Delete([CanBeNull] Expression> filter = null, bool silent = false) + { + using (var context = contextFactory.CreateContext()) + { + var items = context.All() + .Where(s => !s.DeletePending); + + if (filter != null) + items = items.Where(filter); + + scoreModelManager.Delete(items.ToList(), silent); + } + } + public void Delete(List items, bool silent = false) { scoreModelManager.Delete(items, silent); diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index 4970db8955..afae858b46 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -1,14 +1,13 @@ // 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.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Overlays.Dialog; using osu.Game.Scoring; -using System; -using System.Linq; -using System.Threading.Tasks; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Screens.Select { @@ -29,7 +28,7 @@ namespace osu.Game.Screens.Select Text = @"Yes. Please.", Action = () => { - Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID).ToList())) + Task.Run(() => scoreManager.Delete(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID)) .ContinueWith(_ => onCompletion); } }, diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 34129f232c..a83cb08e1f 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; @@ -24,6 +25,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IBindable ruleset { get; set; } + [Resolved] + private RealmContextFactory realmFactory { get; set; } + [Resolved] private IAPIProvider api { get; set; } @@ -54,7 +58,7 @@ namespace osu.Game.Screens.Select.Carousel private void fetchAndLoadTopScore() { - var rank = fetchTopScore()?.Rank; + var rank = fetchTopScoreRank(); scheduledRankUpdate = Schedule(() => { Rank = rank; @@ -67,14 +71,18 @@ namespace osu.Game.Screens.Select.Carousel // We're present if a rank is set, or if there is a pending rank update (IsPresent = true is required for the scheduler to run). public override bool IsPresent => base.IsPresent && (Rank != null || scheduledRankUpdate?.Completed == false); - private ScoreInfo fetchTopScore() + private ScoreRank? fetchTopScoreRank() { - if (scores == null || beatmapInfo == null || ruleset?.Value == null || api?.LocalUser.Value == null) + if (realmFactory == null || beatmapInfo == null || ruleset?.Value == null || api?.LocalUser.Value == null) return null; - return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) - .OrderByDescending(s => s.TotalScore) - .FirstOrDefault(); + using (var realm = realmFactory.CreateContext()) + { + return realm.All().Where(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) + .OrderByDescending(s => s.TotalScore) + .FirstOrDefault() + ?.Rank; + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 7026f240c8..e47acc5ba7 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Leaderboards; @@ -26,6 +27,9 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private RealmContextFactory realmFactory { get; set; } + private BeatmapInfo beatmapInfo; public BeatmapInfo BeatmapInfo @@ -126,26 +130,28 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { - var scores = scoreManager - .QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); - - if (filterMods && !mods.Value.Any()) + using (var realm = realmFactory.CreateContext()) { - // we need to filter out all scores that have any mods to get all local nomod scores - scores = scores.Where(s => !s.Mods.Any()); - } - else if (filterMods) - { - // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters) - // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself - var selectedMods = mods.Value.Select(m => m.Acronym); - scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); - } + var scores = realm.All().Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); - scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) - .ContinueWith(task => scoresCallback?.Invoke(task.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); + if (filterMods && !mods.Value.Any()) + { + // we need to filter out all scores that have any mods to get all local nomod scores + scores = scores.Where(s => !s.Mods.Any()); + } + else if (filterMods) + { + // otherwise find all the scores that have *any* of the currently selected mods (similar to how web applies mod filters) + // we're creating and using a string list representation of selected mods so that it can be translated into the DB query itself + var selectedMods = mods.Value.Select(m => m.Acronym); + scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); + } - return null; + scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) + .ContinueWith(ordered => scoresCallback?.Invoke(ordered.Result), TaskContinuationOptions.OnlyOnRanToCompletion); + + return null; + } } if (api?.IsLoggedIn != true) From d70e292828b80790d0282002cfec4ece06db38a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 19:46:55 +0900 Subject: [PATCH 034/227] Remove old EF classes --- osu.Game/Database/ArchiveModelManager.cs | 838 ------------------ osu.Game/Database/DatabaseBackedStore.cs | 52 -- .../Database/MutableDatabaseBackedStore.cs | 161 ---- ...ableDatabaseBackedStoreWithFileIncludes.cs | 27 - osu.Game/Stores/RealmArchiveModelManager.cs | 2 +- 5 files changed, 1 insertion(+), 1079 deletions(-) delete mode 100644 osu.Game/Database/ArchiveModelManager.cs delete mode 100644 osu.Game/Database/DatabaseBackedStore.cs delete mode 100644 osu.Game/Database/MutableDatabaseBackedStore.cs delete mode 100644 osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs deleted file mode 100644 index 9c26451d40..0000000000 --- a/osu.Game/Database/ArchiveModelManager.cs +++ /dev/null @@ -1,838 +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 System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Humanizer; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; -using osu.Framework.Extensions; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Framework.Threading; -using osu.Game.Extensions; -using osu.Game.IO; -using osu.Game.IO.Archives; -using osu.Game.IPC; -using osu.Game.Overlays.Notifications; - -namespace osu.Game.Database -{ - /// - /// Encapsulates a model store class to give it import functionality. - /// Adds cross-functionality with to give access to the central file store for the provided model. - /// - /// The model type. - /// The associated file join type. - public abstract class ArchiveModelManager : IModelImporter, IModelManager, IModelFileManager - where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete - where TFileModel : class, INamedFileInfo, IHasPrimaryKey, new() - { - private const int import_queue_request_concurrency = 1; - - /// - /// The size of a batch import operation before considering it a lower priority operation. - /// - private const int low_priority_import_batch_size = 1; - - /// - /// A singleton scheduler shared by all . - /// - /// - /// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly. - /// It is mainly being used as a queue mechanism for large imports. - /// - private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); - - /// - /// A second scheduler for lower priority imports. - /// For simplicity, these will just run in parallel with normal priority imports, but a future refactor would see this implemented via a custom scheduler/queue. - /// See https://gist.github.com/peppy/f0e118a14751fc832ca30dd48ba3876b for an incomplete version of this. - /// - private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); - - public Action PostNotification { protected get; set; } - - /// - /// Fired when a new or updated becomes available in the database. - /// This is not guaranteed to run on the update thread. - /// - public event Action ItemUpdated; - - /// - /// Fired when a is removed from the database. - /// This is not guaranteed to run on the update thread. - /// - public event Action ItemRemoved; - - public virtual IEnumerable HandledExtensions => new[] { @".zip" }; - - protected readonly FileStore Files; - - protected readonly IDatabaseContextFactory ContextFactory; - - protected readonly MutableDatabaseBackedStore ModelStore; - - // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) - private ArchiveImportIPCChannel ipc; - - protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes modelStore, IIpcHost importHost = null) - { - ContextFactory = contextFactory; - - ModelStore = modelStore; - ModelStore.ItemUpdated += item => handleEvent(() => ItemUpdated?.Invoke(item)); - ModelStore.ItemRemoved += item => handleEvent(() => ItemRemoved?.Invoke(item)); - - Files = new FileStore(contextFactory, storage); - - if (importHost != null) - ipc = new ArchiveImportIPCChannel(importHost, this); - - ModelStore.Cleanup(); - } - - /// - /// Import one or more items from filesystem . - /// - /// - /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. - /// This will post notifications tracking progress. - /// - /// One or more archive locations on disk. - public Task Import(params string[] paths) - { - var notification = new ImportProgressNotification(); - - PostNotification?.Invoke(notification); - - return Import(notification, paths.Select(p => new ImportTask(p)).ToArray()); - } - - public Task Import(params ImportTask[] tasks) - { - var notification = new ImportProgressNotification(); - - PostNotification?.Invoke(notification); - - return Import(notification, tasks); - } - - public async Task>> Import(ProgressNotification notification, params ImportTask[] tasks) - { - if (tasks.Length == 0) - { - notification.CompletionText = $"No {HumanisedModelName}s were found to import!"; - notification.State = ProgressNotificationState.Completed; - return Enumerable.Empty>(); - } - - notification.Progress = 0; - notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising..."; - - int current = 0; - - var imported = new List>(); - - bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size; - - try - { - await Task.WhenAll(tasks.Select(async task => - { - notification.CancellationToken.ThrowIfCancellationRequested(); - - try - { - var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false); - - lock (imported) - { - if (model != null) - imported.Add(model); - current++; - - notification.Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s"; - notification.Progress = (float)current / tasks.Length; - } - } - catch (TaskCanceledException) - { - throw; - } - catch (Exception e) - { - Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database); - } - })).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - if (imported.Count == 0) - { - notification.State = ProgressNotificationState.Cancelled; - return imported; - } - } - - if (imported.Count == 0) - { - notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!"; - notification.State = ProgressNotificationState.Cancelled; - } - else - { - notification.CompletionText = imported.Count == 1 - ? $"Imported {imported.First().Value.GetDisplayString()}!" - : $"Imported {imported.Count} {HumanisedModelName}s!"; - - if (imported.Count > 0 && PostImport != null) - { - notification.CompletionText += " Click to view."; - notification.CompletionClickAction = () => - { - PostImport?.Invoke(imported); - return true; - }; - } - - notification.State = ProgressNotificationState.Completed; - } - - return imported; - } - - /// - /// Import one from the filesystem and delete the file on success. - /// Note that this bypasses the UI flow and should only be used for special cases or testing. - /// - /// The containing data about the to import. - /// Whether this is a low priority import. - /// An optional cancellation token. - /// The imported model, if successful. - public async Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - ILive import; - using (ArchiveReader reader = task.GetReader()) - import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false); - - // We may or may not want to delete the file depending on where it is stored. - // e.g. reconstructing/repairing database with items from default storage. - // Also, not always a single file, i.e. for LegacyFilesystemReader - // TODO: Add a check to prevent files from storage to be deleted. - try - { - if (import != null && File.Exists(task.Path) && ShouldDeleteArchive(task.Path)) - File.Delete(task.Path); - } - catch (Exception e) - { - LogForModel(import?.Value, $@"Could not delete original file after import ({task})", e); - } - - return import; - } - - public Action>> PostImport { protected get; set; } - - /// - /// Silently import an item from an . - /// - /// The archive to be imported. - /// Whether this is a low priority import. - /// An optional cancellation token. - public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - TModel model = null; - - try - { - model = CreateModel(archive); - - if (model == null) - return Task.FromResult>(null); - } - catch (TaskCanceledException) - { - throw; - } - catch (Exception e) - { - LogForModel(model, @$"Model creation of {archive.Name} failed.", e); - return null; - } - - return Import(model, archive, lowPriority, cancellationToken); - } - - /// - /// Any file extensions which should be included in hash creation. - /// Generally should include all file types which determine the file's uniqueness. - /// Large files should be avoided if possible. - /// - /// - /// This is only used by the default hash implementation. If is overridden, it will not be used. - /// - protected abstract string[] HashableFileTypes { get; } - - internal static void LogForModel(TModel model, string message, Exception e = null) - { - string prefix = $"[{(model?.Hash ?? "?????").Substring(0, 5)}]"; - - if (e != null) - Logger.Error(e, $"{prefix} {message}", LoggingTarget.Database); - else - Logger.Log($"{prefix} {message}", LoggingTarget.Database); - } - - /// - /// Whether the implementation overrides with a custom implementation. - /// Custom hash implementations must bypass the early exit in the import flow (see usage). - /// - protected virtual bool HasCustomHashFunction => false; - - /// - /// Create a SHA-2 hash from the provided archive based on file content of all files matching . - /// - /// - /// In the case of no matching files, a hash will be generated from the passed archive's . - /// - protected virtual string ComputeHash(TModel item) - { - var hashableFiles = item.Files - .Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) - .OrderBy(f => f.Filename) - .ToArray(); - - if (hashableFiles.Length > 0) - { - // for now, concatenate all hashable files in the set to create a unique hash. - MemoryStream hashable = new MemoryStream(); - - foreach (TFileModel file in hashableFiles) - { - using (Stream s = Files.Store.GetStream(file.FileInfo.GetStoragePath())) - s.CopyTo(hashable); - } - - if (hashable.Length > 0) - return hashable.ComputeSHA2Hash(); - } - - return generateFallbackHash(); - } - - /// - /// Silently import an item from a . - /// - /// The model to be imported. - /// An optional archive to use for model population. - /// Whether this is a low priority import. - /// An optional cancellation token. - public virtual async Task> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => - { - cancellationToken.ThrowIfCancellationRequested(); - - bool checkedExisting = false; - TModel existing = null; - - if (archive != null && !HasCustomHashFunction) - { - // this is a fast bail condition to improve large import performance. - item.Hash = computeHashFast(archive); - - checkedExisting = true; - existing = CheckForExisting(item); - - if (existing != null) - { - // bare minimum comparisons - // - // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. - // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. - if (CanSkipImport(existing, item) && - getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) - { - LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); - Undelete(existing); - return existing.ToEntityFrameworkLive(); - } - - LogForModel(item, @"Found existing (optimised) but failed pre-check."); - } - } - - void rollback() - { - if (!Delete(item)) - { - // We may have not yet added the model to the underlying table, but should still clean up files. - LogForModel(item, @"Dereferencing files for incomplete import."); - Files.Dereference(item.Files.Select(f => f.FileInfo).ToArray()); - } - } - - delayEvents(); - - try - { - LogForModel(item, @"Beginning import..."); - - if (archive != null) - item.Files.AddRange(createFileInfos(archive, Files)); - item.Hash = ComputeHash(item); - - await Populate(item, archive, cancellationToken).ConfigureAwait(false); - - using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. - { - try - { - if (!write.IsTransactionLeader) throw new InvalidOperationException(@$"Ensure there is no parent transaction so errors can correctly be handled by {this}"); - - if (!checkedExisting) - existing = CheckForExisting(item); - - if (existing != null) - { - if (CanReuseExisting(existing, item)) - { - Undelete(existing); - LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); - // existing item will be used; rollback new import and exit early. - rollback(); - flushEvents(true); - return existing.ToEntityFrameworkLive(); - } - - LogForModel(item, @"Found existing but failed re-use check."); - Delete(existing); - ModelStore.PurgeDeletable(s => s.ID == existing.ID); - } - - PreImport(item); - - // import to store - ModelStore.Add(item); - } - catch (Exception e) - { - write.Errors.Add(e); - throw; - } - } - - LogForModel(item, @"Import successfully completed!"); - } - catch (Exception e) - { - if (!(e is TaskCanceledException)) - LogForModel(item, @"Database import or population failed and has been rolled back.", e); - - rollback(); - flushEvents(false); - throw; - } - - flushEvents(true); - return item.ToEntityFrameworkLive(); - }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false); - - /// - /// Replace an existing file with a new version. - /// - /// The item to operate on. - /// The existing file to be replaced. - /// The new file contents. - public void ReplaceFile(TModel model, TFileModel file, Stream contents) - { - using (ContextFactory.GetForWrite()) - { - DeleteFile(model, file); - AddFile(model, contents, file.Filename); - } - } - - /// - /// Delete an existing file. - /// - /// The item to operate on. - /// The existing file to be deleted. - public void DeleteFile(TModel model, TFileModel file) - { - using (var usage = ContextFactory.GetForWrite()) - { - // Dereference the existing file info, since the file model will be removed. - if (file.FileInfo != null) - { - Files.Dereference(file.FileInfo); - - if (file.IsManaged) - { - // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked - // Definitely can be removed once we rework the database backend. - usage.Context.Set().Remove(file); - } - } - - model.Files.Remove(file); - } - } - - /// - /// Add a new file. - /// - /// The item to operate on. - /// The new file contents. - /// The filename for the new file. - public void AddFile(TModel model, Stream contents, string filename) - { - using (ContextFactory.GetForWrite()) - { - model.Files.Add(new TFileModel - { - Filename = filename, - FileInfo = Files.Add(contents) - }); - } - - if (model.IsManaged) - Update(model); - } - - /// - /// Perform an update of the specified item. - /// TODO: Support file additions/removals. - /// - /// The item to update. - public void Update(TModel item) - { - using (ContextFactory.GetForWrite()) - { - item.Hash = ComputeHash(item); - ModelStore.Update(item); - } - } - - /// - /// Delete an item from the manager. - /// Is a no-op for already deleted items. - /// - /// The item to delete. - /// false if no operation was performed - public bool Delete(TModel item) - { - using (ContextFactory.GetForWrite()) - { - // re-fetch the model on the import context. - var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).FirstOrDefault(s => s.ID == item.ID); - - if (foundModel == null || foundModel.DeletePending) return false; - - if (ModelStore.Delete(foundModel)) - Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray()); - return true; - } - } - - /// - /// Delete multiple items. - /// This will post notifications tracking progress. - /// - public void Delete(List items, bool silent = false) - { - if (items.Count == 0) return; - - var notification = new ProgressNotification - { - Progress = 0, - Text = $"Preparing to delete all {HumanisedModelName}s...", - CompletionText = $"Deleted all {HumanisedModelName}s!", - State = ProgressNotificationState.Active, - }; - - if (!silent) - PostNotification?.Invoke(notification); - - int i = 0; - - foreach (var b in items) - { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; - - notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})"; - - Delete(b); - - notification.Progress = (float)i / items.Count; - } - - notification.State = ProgressNotificationState.Completed; - } - - /// - /// Restore multiple items that were previously deleted. - /// This will post notifications tracking progress. - /// - public void Undelete(List items, bool silent = false) - { - if (!items.Any()) return; - - var notification = new ProgressNotification - { - CompletionText = "Restored all deleted items!", - Progress = 0, - State = ProgressNotificationState.Active, - }; - - if (!silent) - PostNotification?.Invoke(notification); - - int i = 0; - - foreach (var item in items) - { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; - - notification.Text = $"Restoring ({++i} of {items.Count})"; - - Undelete(item); - - notification.Progress = (float)i / items.Count; - } - - notification.State = ProgressNotificationState.Completed; - } - - /// - /// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set. - /// - /// The item to restore - public void Undelete(TModel item) - { - using (var usage = ContextFactory.GetForWrite()) - { - usage.Context.ChangeTracker.AutoDetectChangesEnabled = false; - - if (!ModelStore.Undelete(item)) return; - - Files.Reference(item.Files.Select(f => f.FileInfo).ToArray()); - - usage.Context.ChangeTracker.AutoDetectChangesEnabled = true; - } - } - - private string computeHashFast(ArchiveReader reader) - { - MemoryStream hashable = new MemoryStream(); - - foreach (string file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f)) - { - using (Stream s = reader.GetStream(file)) - s.CopyTo(hashable); - } - - if (hashable.Length > 0) - return hashable.ComputeSHA2Hash(); - - return generateFallbackHash(); - } - - /// - /// Create all required s for the provided archive, adding them to the global file store. - /// - private List createFileInfos(ArchiveReader reader, FileStore files) - { - var fileInfos = new List(); - - // import files to manager - foreach (var filenames in getShortenedFilenames(reader)) - { - using (Stream s = reader.GetStream(filenames.original)) - { - fileInfos.Add(new TFileModel - { - Filename = filenames.shortened, - FileInfo = files.Add(s) - }); - } - } - - return fileInfos; - } - - private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader) - { - string prefix = reader.Filenames.GetCommonPrefix(); - if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) - prefix = string.Empty; - - // import files to manager - foreach (string file in reader.Filenames) - yield return (file, file.Substring(prefix.Length).ToStandardisedPath()); - } - - #region osu-stable import - - /// - /// Whether this specified path should be removed after successful import. - /// - /// The path for consideration. May be a file or a directory. - /// Whether to perform deletion. - protected virtual bool ShouldDeleteArchive(string path) => false; - - #endregion - - /// - /// Create a barebones model from the provided archive. - /// Actual expensive population should be done in ; this should just prepare for duplicate checking. - /// - /// The archive to create the model for. - /// A model populated with minimal information. Returning a null will abort importing silently. - protected abstract TModel CreateModel(ArchiveReader archive); - - /// - /// Populate the provided model completely from the given archive. - /// After this method, the model should be in a state ready to commit to a store. - /// - /// The model to populate. - /// The archive to use as a reference for population. May be null. - /// An optional cancellation token. - protected abstract Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default); - - /// - /// Perform any final actions before the import to database executes. - /// - /// The model prepared for import. - protected virtual void PreImport(TModel model) - { - } - - /// - /// Check whether an existing model already exists for a new import item. - /// - /// The new model proposed for import. - /// An existing model which matches the criteria to skip importing, else null. - protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); - - public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, ModelStore.ConsumableItems.Where(m => !m.DeletePending)); - - /// - /// Performs implementation specific comparisons to determine whether a given model is present in the local store. - /// - /// The whose existence needs to be checked. - /// The usable items present in the store. - /// Whether the exists. - protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) - => model.IsManaged && items.Any(i => i.ID == model.ID && i.Files.Any()); - - /// - /// Whether import can be skipped after finding an existing import early in the process. - /// Only valid when is not overridden. - /// - /// The existing model. - /// The newly imported model. - /// Whether to skip this import completely. - protected virtual bool CanSkipImport(TModel existing, TModel import) => true; - - /// - /// After an existing is found during an import process, the default behaviour is to use/restore the existing - /// item and skip the import. This method allows changing that behaviour. - /// - /// The existing model. - /// The newly imported model. - /// Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import. - protected virtual bool CanReuseExisting(TModel existing, TModel import) => - // for the best or worst, we copy and import files of a new import before checking whether - // it is a duplicate. so to check if anything has changed, we can just compare all FileInfo IDs. - getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) && - getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)); - - private IEnumerable getIDs(List files) - { - foreach (var f in files.OrderBy(f => f.Filename)) - yield return f.FileInfo.ID; - } - - private IEnumerable getFilenames(List files) - { - foreach (var f in files.OrderBy(f => f.Filename)) - yield return f.Filename; - } - - private DbSet queryModel() => ContextFactory.Get().Set(); - - public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; - - #region Event handling / delaying - - private readonly List queuedEvents = new List(); - - /// - /// Allows delaying of outwards events until an operation is confirmed (at a database level). - /// - private bool delayingEvents; - - /// - /// Begin delaying outwards events. - /// - private void delayEvents() => delayingEvents = true; - - /// - /// Flush delayed events and disable delaying. - /// - /// Whether the flushed events should be performed. - private void flushEvents(bool perform) - { - Action[] events; - - lock (queuedEvents) - { - events = queuedEvents.ToArray(); - queuedEvents.Clear(); - } - - if (perform) - { - foreach (var a in events) - a.Invoke(); - } - - delayingEvents = false; - } - - private void handleEvent(Action a) - { - if (delayingEvents) - { - lock (queuedEvents) - queuedEvents.Add(a); - } - else - a.Invoke(); - } - - #endregion - - private static string generateFallbackHash() - { - // if a hash could no be generated from file content, presume a unique / new import. - // therefore, let's use a guaranteed unique hash. - // this doesn't follow the SHA2 hashing schema intentionally, so such entries on the data store can be identified. - return Guid.NewGuid().ToString(); - } - } -} diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs deleted file mode 100644 index a37155ee62..0000000000 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ /dev/null @@ -1,52 +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 System.Linq; -using Microsoft.EntityFrameworkCore; -using osu.Framework.Platform; - -namespace osu.Game.Database -{ - public abstract class DatabaseBackedStore - { - protected readonly Storage Storage; - - protected readonly RealmContextFactory ContextFactory; - - /// - /// Refresh an instance potentially from a different thread with a local context-tracked instance. - /// - /// 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 void Refresh(ref T obj, IQueryable lookupSource = null) where T : class, IHasPrimaryKey - { - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; - - if (context.Entry(obj).State != EntityState.Detached) return; - - int id = obj.ID; - var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); - if (foundObject != null) - obj = foundObject; - else - context.Add(obj); - } - } - - protected DatabaseBackedStore(RealmContextFactory contextFactory, Storage storage = null) - { - ContextFactory = contextFactory; - Storage = storage; - } - - /// - /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. - /// - public virtual void Cleanup() - { - } - } -} diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs deleted file mode 100644 index b0feb7bb78..0000000000 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ /dev/null @@ -1,161 +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 System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using osu.Framework.Platform; - -namespace osu.Game.Database -{ - /// - /// A typed store which supports basic addition, deletion and updating for soft-deletable models. - /// - /// The databased model. - public abstract class MutableDatabaseBackedStore : DatabaseBackedStore - where T : class, IHasPrimaryKey, ISoftDelete - { - /// - /// Fired when an item was added or updated. - /// - public event Action ItemUpdated; - - /// - /// Fired when an item was removed. - /// - public event Action ItemRemoved; - - protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) - : base(contextFactory, storage) - { - } - - /// - /// Access items pre-populated with includes for consumption. - /// - public IQueryable ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set()); - - /// - /// Access barebones items with no includes. - /// - public IQueryable Items => ContextFactory.Get().Set(); - - /// - /// Add a to the database. - /// - /// The item to add. - public void Add(T item) - { - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; - context.Attach(item); - } - - ItemUpdated?.Invoke(item); - } - - /// - /// Update a in the database. - /// - /// The item to update. - public void Update(T item) - { - using (var usage = ContextFactory.GetForWrite()) - usage.Context.Update(item); - - ItemUpdated?.Invoke(item); - } - - /// - /// Delete a from the database. - /// - /// The item to delete. - public bool Delete(T item) - { - using (ContextFactory.GetForWrite()) - { - Refresh(ref item); - - if (item.DeletePending) return false; - - item.DeletePending = true; - } - - ItemRemoved?.Invoke(item); - return true; - } - - /// - /// Restore a from a deleted state. - /// - /// The item to undelete. - public bool Undelete(T item) - { - using (ContextFactory.GetForWrite()) - { - Refresh(ref item, ConsumableItems); - - if (!item.DeletePending) return false; - - item.DeletePending = false; - } - - ItemUpdated?.Invoke(item); - return true; - } - - /// - /// Allow implementations to add database-side includes or constraints when querying for consumption of items. - /// - /// The input query. - /// A potentially modified output query. - protected virtual IQueryable AddIncludesForConsumption(IQueryable query) => query; - - /// - /// Allow implementations to add database-side includes or constraints when deleting items. - /// Included properties could then be subsequently deleted by overriding . - /// - /// The input query. - /// A potentially modified output query. - protected virtual IQueryable AddIncludesForDeletion(IQueryable query) => query; - - /// - /// Called when removing an item completely from the database. - /// - /// The items to be purged. - /// The write context which can be used to perform subsequent deletions. - protected virtual void Purge(List items, OsuDbContext context) => context.RemoveRange(items); - - public override void Cleanup() - { - base.Cleanup(); - PurgeDeletable(); - } - - /// - /// Purge items in a pending delete state. - /// - /// An optional query limiting the scope of the purge. - public void PurgeDeletable(Expression> query = null) - { - using (var usage = ContextFactory.GetForWrite()) - { - var context = usage.Context; - - var lookup = context.Set().Where(s => s.DeletePending); - - if (query != null) lookup = lookup.Where(query); - - lookup = AddIncludesForDeletion(lookup); - - var purgeable = lookup.ToList(); - - if (!purgeable.Any()) return; - - Purge(purgeable, context); - } - } - } -} diff --git a/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs b/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs deleted file mode 100644 index 102081cd65..0000000000 --- a/osu.Game/Database/MutableDatabaseBackedStoreWithFileIncludes.cs +++ /dev/null @@ -1,27 +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 System.Linq; -using Microsoft.EntityFrameworkCore; -using osu.Framework.Platform; - -namespace osu.Game.Database -{ - public abstract class MutableDatabaseBackedStoreWithFileIncludes : MutableDatabaseBackedStore - where T : class, IHasPrimaryKey, ISoftDelete, IHasFiles - where TFileInfo : INamedFileInfo - { - protected MutableDatabaseBackedStoreWithFileIncludes(IDatabaseContextFactory contextFactory, Storage storage = null) - : base(contextFactory, storage) - { - } - - protected override IQueryable AddIncludesForConsumption(IQueryable query) => - base.AddIncludesForConsumption(query) - .Include(s => s.Files).ThenInclude(f => f.FileInfo); - - protected override IQueryable AddIncludesForDeletion(IQueryable query) => - base.AddIncludesForDeletion(query) - .Include(s => s.Files); // don't include FileInfo. these are handled by the FileStore itself. - } -} diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 87a27cbbbc..7fa8d16882 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -19,7 +19,7 @@ using Realms; namespace osu.Game.Stores { /// - /// Class which adds all the missing pieces bridging the gap between and . + /// Class which adds all the missing pieces bridging the gap between and (legacy) ArchiveModelManager. /// public abstract class RealmArchiveModelManager : RealmArchiveModelImporter, IModelManager, IModelFileManager where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete From a3da8dc49d416d4008b4a6dc0b4d75e74211d147 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 19:52:54 +0900 Subject: [PATCH 035/227] Fix missing interface implementation of `IRulesetStore` --- osu.Game/Rulesets/RulesetStore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index de3bb1ac34..6ec89f9dc5 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -16,7 +16,7 @@ using osu.Game.Database; namespace osu.Game.Rulesets { - public class RulesetStore : IDisposable + public class RulesetStore : IDisposable, IRulesetStore { private readonly RealmContextFactory realmFactory; @@ -259,8 +259,8 @@ namespace osu.Game.Rulesets #region Implementation of IRulesetStore - IRulesetInfo IRulesetStore.GetRuleset(int id) => GetRuleset(id); - IRulesetInfo IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName); + IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id); + IRulesetInfo? IRulesetStore.GetRuleset(string shortName) => GetRuleset(shortName); IEnumerable IRulesetStore.AvailableRulesets => AvailableRulesets; #endregion From 00e3af3366e80214c7f0c956d144000a78cd0b56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 19:47:11 +0900 Subject: [PATCH 036/227] Update model manager and many related classes to get things compiling again --- .../Database/BeatmapImporterTests.cs | 42 +- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 8 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 4 +- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerPlaylist.cs | 4 +- .../TestSceneMultiplayerQueueList.cs | 4 +- .../TestSceneMultiplayerReadyButton.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 2 +- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 4 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 3 +- .../TestSceneBeatmapMetadataDisplay.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 5 +- osu.Game/Beatmaps/BeatmapManager.cs | 28 +- osu.Game/Beatmaps/BeatmapModelManager.cs | 399 +++--------------- osu.Game/Beatmaps/EFBeatmapSetInfo.cs | 4 +- osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 4 +- osu.Game/Database/ImportTask.cs | 3 +- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Scoring/LegacyDatabasedScore.cs | 4 +- osu.Game/Scoring/ScoreInfo.cs | 20 +- osu.Game/Scoring/ScoreManager.cs | 1 - osu.Game/Scoring/ScoreModelManager.cs | 5 +- osu.Game/Screens/Menu/IntroScreen.cs | 4 +- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 10 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- osu.Game/Stores/BeatmapImporter.cs | 4 +- .../Drawables/DrawableStoryboard.cs | 4 +- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 2 - osu.Game/Tests/Visual/EditorTestScene.cs | 8 +- 34 files changed, 165 insertions(+), 431 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index c5dbcf155c..e144f863b0 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using (var importer = new BeatmapImporter(realmFactory, storage)) + using (var importer = new BeatmapModelManager(realmFactory, storage)) using (new RulesetStore(realmFactory, storage)) { ILive? imported; @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); await LoadOszIntoStore(importer, realmFactory.Context); @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? tempPath = TestResources.GetTestBeatmapForImport(); @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -212,7 +212,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -264,7 +264,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -312,7 +312,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -361,7 +361,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -394,7 +394,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var progressNotification = new ImportProgressNotification(); @@ -430,7 +430,7 @@ namespace osu.Game.Tests.Database Interlocked.Increment(ref loggedExceptionCount); }; - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -480,7 +480,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -528,7 +528,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var imported = await LoadOszIntoStore(importer, realmFactory.Context); @@ -554,7 +554,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); var metadata = new BeatmapMetadata @@ -600,7 +600,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -617,7 +617,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -653,7 +653,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -695,7 +695,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -746,7 +746,7 @@ namespace osu.Game.Tests.Database { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new BeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); string? temp = TestResources.GetTestBeatmapForImport(); diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 2cab823526..5a2349e776 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -172,17 +172,17 @@ namespace osu.Game.Tests.Online { } - protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) { - return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); + return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, onlineLookupQueue); } internal class TestBeatmapModelManager : BeatmapModelManager { private readonly TestBeatmapManager testBeatmapManager; - public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) - : base(storage, databaseContextFactory, rulesetStore, gameHost) + public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) + : base(databaseContextFactory, storage, beatmapOnlineLookupQueue) { this.testBeatmapManager = testBeatmapManager; } diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index c4d7bd7e6a..a993190a67 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); InitialBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 3d8c5298dc..18f45086f6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); }); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); @@ -588,7 +588,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("restore beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); }); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is SpectatorScreen); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 52e46ef5af..6b6a02906e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); } [SetUp] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 464c0ea5b6..251f407a85 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); }); @@ -169,7 +169,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem { Beatmap = { Value = importedBeatmap }, - BeatmapID = importedBeatmap.OnlineID ?? -1, + BeatmapID = importedBeatmap.OnlineID, Expired = expired, PlayedAt = DateTimeOffset.Now }))); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 29daff546d..ad47745642 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); }); @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Multiplayer MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem { Beatmap = { Value = importedBeatmap }, - BeatmapID = importedBeatmap.OnlineID ?? -1, + BeatmapID = importedBeatmap.OnlineID, }); Client.AddUserPlaylistItem(userId(), item); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 8f51b1e381..59e5318263 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AvailabilityTracker.SelectedItem.BindTo(selectedItem); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); selectedItem.Value = new PlaylistItem { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index d4ff9f8c41..5466a19657 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AvailabilityTracker.SelectedItem.BindTo(selectedItem); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); selectedItem.Value = new PlaylistItem { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index 93ccd5f1e1..e63e58824f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -12,7 +12,7 @@ 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.Models; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Metadata = new BeatmapMetadata { Artist = "Artist", - Author = new APIUser { Username = "Creator name here" }, + Author = new RealmUser { Username = "Creator name here" }, Title = "Long title used to check background colour", }, BeatmapSet = new BeatmapSetInfo() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index d20fbd3539..2140acb2e3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); }); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 62b0f10fd8..a5f42545c1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -20,7 +20,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Stores; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -45,7 +44,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(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); return dependencies; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs index e573c96ce9..fb6d9a0b4b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.SongSelect { showMetadataForBeatmap(() => { - var allBeatmapSets = manager.GetAllUsableBeatmapSets(IncludedDetails.Minimal); + var allBeatmapSets = manager.GetAllUsableBeatmapSets(); if (allBeatmapSets.Count == 0) return manager.DefaultBeatmap; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 5c4759a466..49200f6e1a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -5,10 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Graphics; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; using osu.Framework.Testing; @@ -24,7 +24,6 @@ using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Stores; using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; @@ -89,7 +88,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(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); beatmapInfo = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely().Value.Beatmaps[0]; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 5c8cf38682..45ae69e4ba 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -16,7 +16,6 @@ using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; -using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Online.API; @@ -43,21 +42,18 @@ namespace osu.Game.Beatmaps public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) { + if (performOnlineLookups) + onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + var userResources = new RealmFileStore(contextFactory, storage).Store; BeatmapTrackStore = audioManager.GetTrackStore(userResources); - beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host); + beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, onlineBeatmapLookupQueue); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); workingBeatmapCache.BeatmapManager = beatmapModelManager; beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; - - if (performOnlineLookups) - { - onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - beatmapModelManager.OnlineLookupQueue = onlineBeatmapLookupQueue; - } } protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) @@ -65,8 +61,8 @@ namespace osu.Game.Beatmaps return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host); } - protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) => - new BeatmapModelManager(storage, contextFactory, rulesets, host); + protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, [CanBeNull] BeatmapOnlineLookupQueue onlineLookupQueue) => + new BeatmapModelManager(contextFactory, storage, onlineLookupQueue); /// /// Create a new . @@ -97,9 +93,9 @@ namespace osu.Game.Beatmaps } }; - var imported = beatmapModelManager.Import(set).GetResultSafely().Value; + var imported = beatmapModelManager.Import(set).GetResultSafely()?.Value; - return GetWorkingBeatmap(imported.Beatmaps.First()); + return GetWorkingBeatmap(imported?.Beatmaps.First()); } #region Delegation to BeatmapModelManager (methods which previously existed locally). @@ -135,23 +131,21 @@ namespace osu.Game.Beatmaps /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includes, includeProtected); + public List GetAllUsableBeatmapSets(bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includeProtected); /// /// Returns a list of all usable s. Note that files are not populated. /// - /// The level of detail to include in the returned objects. /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. /// A list of available . - public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includes, includeProtected); + public IEnumerable GetAllUsableBeatmapSetsEnumerable(bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includeProtected); /// /// Perform a lookup query on available s. /// /// The query. - /// The level of detail to include in the returned objects. /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) => beatmapModelManager.QueryBeatmapSets(query, includes); + public IEnumerable QueryBeatmapSets(Expression> query) => beatmapModelManager.QueryBeatmapSets(query); /// /// Perform a lookup query on available s. diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index f6f2e16410..100189bf09 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -3,30 +3,21 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using osu.Framework.Audio.Track; using osu.Framework.Extensions; -using osu.Framework.Graphics.Textures; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.IO; -using osu.Game.IO.Archives; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; using osu.Game.Skinning; using osu.Game.Stores; -using Realms; -using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +#nullable enable namespace osu.Game.Beatmaps { @@ -34,145 +25,65 @@ namespace osu.Game.Beatmaps /// Handles ef-core storage of beatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapModelManager : RealmArchiveModelManager + public class BeatmapModelManager : BeatmapImporter { /// /// Fired when a single difficulty has been hidden. /// - public event Action BeatmapHidden; + public event Action? BeatmapHidden; /// /// Fired when a single difficulty has been restored. /// - public event Action BeatmapRestored; - - /// - /// An online lookup queue component which handles populating online beatmap metadata. - /// - public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; } + public event Action? BeatmapRestored; /// /// The game working beatmap cache, used to invalidate entries on changes. /// - public IWorkingBeatmapCache WorkingBeatmapCache { private get; set; } + public IWorkingBeatmapCache? WorkingBeatmapCache { private get; set; } public override IEnumerable HandledExtensions => new[] { ".osz" }; protected override string[] HashableFileTypes => new[] { ".osu" }; - private readonly RulesetStore rulesets; - - public BeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, GameHost host = null) - : base(storage, contextFactory) + public BeatmapModelManager(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) + : base(contextFactory, storage, onlineLookupQueue) { - this.rulesets = rulesets; - - // beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - // beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - // beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); - // beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); } protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken) - { - if (archive != null) - beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files)); - - foreach (BeatmapInfo b in beatmapSet.Beatmaps) - { - // remove metadata from difficulties where it matches the set - if (beatmapSet.Metadata.Equals(b.Metadata)) - b.Metadata = null; - - b.BeatmapSet = beatmapSet; - } - - validateOnlineIds(beatmapSet); - - bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0); - - if (OnlineLookupQueue != null) - await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); - - // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. - if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0)) - { - if (beatmapSet.OnlineID != null) - { - beatmapSet.OnlineID = -1; - LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); - } - } - } - - protected override void PreImport(BeatmapSetInfo beatmapSet) - { - if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) - throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); - - // check if a set already exists with the same online id, delete if it does. - if (beatmapSet.OnlineID != null) - { - var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineID == beatmapSet.OnlineID); - - if (existingSetWithSameOnlineID != null) - { - Delete(existingSetWithSameOnlineID); - - // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. - existingSetWithSameOnlineID.OnlineID = -1; - foreach (var b in existingSetWithSameOnlineID.Beatmaps) - b.OnlineID = -1; - - LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineID}). It has been deleted."); - } - } - } - - private void validateOnlineIds(BeatmapSetInfo beatmapSet) - { - var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID.HasValue).Select(b => b.OnlineID).ToList(); - - // ensure all IDs are unique - if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) - { - LogForModel(beatmapSet, "Found non-unique IDs, resetting..."); - resetIds(); - return; - } - - // find any existing beatmaps in the database that have matching online ids - var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineID)).ToList(); - - if (existingBeatmaps.Count > 0) - { - // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. - // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. - var existing = CheckForExisting(beatmapSet); - - if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) - { - LogForModel(beatmapSet, "Found existing import with IDs already, resetting..."); - resetIds(); - } - } - - void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = -1); - } - /// /// Delete a beatmap difficulty. /// /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmapInfo) => beatmaps.Hide(beatmapInfo); + public void Hide(BeatmapInfo beatmapInfo) + { + using (var realm = ContextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + beatmapInfo.Hidden = true; + transaction.Commit(); + + BeatmapHidden?.Invoke(beatmapInfo); + } + } /// /// Restore a beatmap difficulty. /// /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmapInfo) => beatmaps.Restore(beatmapInfo); + public void Restore(BeatmapInfo beatmapInfo) + { + using (var realm = ContextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + beatmapInfo.Hidden = false; + transaction.Commit(); + + BeatmapRestored?.Invoke(beatmapInfo); + } + } /// /// Saves an file against a given . @@ -180,10 +91,12 @@ namespace osu.Game.Beatmaps /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null) + public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) { var setInfo = beatmapInfo.BeatmapSet; + Debug.Assert(setInfo != null); + // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`. // This should hopefully be temporary, assuming said clone is eventually removed. @@ -202,25 +115,29 @@ namespace osu.Game.Beatmaps stream.Seek(0, SeekOrigin.Begin); - using (ContextFactory.GetForWrite()) + using (var realm = ContextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) { beatmapInfo = setInfo.Beatmaps.Single(b => b.Equals(beatmapInfo)); - var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; - // grab the original file (or create a new one if not found). - var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); + var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); + + if (existingFileInfo != null) + { + DeleteFile(setInfo, existingFileInfo); + } // metadata may have changed; update the path with the standard format. - beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename(); + var metadata = beatmapInfo.Metadata; + string filename = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename(); beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - // update existing or populate new file's filename. - fileInfo.Filename = beatmapInfo.Path; - stream.Seek(0, SeekOrigin.Begin); - ReplaceFile(setInfo, fileInfo, stream); + AddFile(setInfo, stream, filename, realm); + + transaction.Commit(); } } @@ -232,100 +149,43 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); - - protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) + public BeatmapSetInfo? QueryBeatmapSet(Expression> query) { - if (!base.CanSkipImport(existing, import)) - return false; - - return existing.Beatmaps.Any(b => b.OnlineID != null); - } - - protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) - { - if (!base.CanReuseExisting(existing, import)) - return false; - - var existingIds = existing.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i); - var importIds = import.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i); - - // force re-import if we are not in a sane state. - return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds); + using (var context = ContextFactory.CreateContext()) + return context.All().FirstOrDefault(query); // TODO: ?.ToLive(); } /// /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => - GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList(); + public List GetAllUsableBeatmapSets(bool includeProtected = false) => + GetAllUsableBeatmapSetsEnumerable(includeProtected).ToList(); /// /// Returns a list of all usable s. Note that files are not populated. /// - /// The level of detail to include in the returned objects. /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. /// A list of available . - public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) + public IEnumerable GetAllUsableBeatmapSetsEnumerable(bool includeProtected = false) { - IQueryable queryable; - - switch (includes) - { - case IncludedDetails.Minimal: - queryable = beatmaps.BeatmapSetsOverview; - break; - - case IncludedDetails.AllButRuleset: - queryable = beatmaps.BeatmapSetsWithoutRuleset; - break; - - case IncludedDetails.AllButFiles: - queryable = beatmaps.BeatmapSetsWithoutFiles; - break; - - default: - queryable = beatmaps.ConsumableItems; - break; - } - - // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY - // clause which causes queries to take 5-10x longer. - // TODO: remove if upgrading to EF core 3.x. - return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected)); + using (var context = ContextFactory.CreateContext()) + return context.All().Where(b => !b.DeletePending && (includeProtected || !b.Protected)); } /// /// Perform a lookup query on available s. /// /// The query. - /// The level of detail to include in the returned objects. /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) + public IEnumerable QueryBeatmapSets(Expression> query) { - IQueryable queryable; - - switch (includes) + using (var context = ContextFactory.CreateContext()) { - case IncludedDetails.Minimal: - queryable = beatmaps.BeatmapSetsOverview; - break; - - case IncludedDetails.AllButRuleset: - queryable = beatmaps.BeatmapSetsWithoutRuleset; - break; - - case IncludedDetails.AllButFiles: - queryable = beatmaps.BeatmapSetsWithoutFiles; - break; - - default: - queryable = beatmaps.ConsumableItems; - break; + return context.All() + .Where(b => !b.DeletePending) + .Where(query); } - - return queryable.AsNoTracking().Where(query); } /// @@ -333,145 +193,24 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapInfo QueryBeatmap(Expression> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query); + public BeatmapInfo? QueryBeatmap(Expression> query) + { + using (var context = ContextFactory.CreateContext()) + return context.All().FirstOrDefault(query); // TODO: ?.ToLive(); + } /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. - public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); - - public override string HumanisedModelName => "beatmap"; - - protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineID != null && items.Any(b => b.OnlineID == model.OnlineID)); - - protected override BeatmapSetInfo CreateModel(ArchiveReader reader) + public IQueryable QueryBeatmaps(Expression> query) { - // let's make sure there are actually .osu files to import. - string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); - - if (string.IsNullOrEmpty(mapName)) + using (var context = ContextFactory.CreateContext()) { - Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); - return null; + return context.All() + .Where(query); } - - Beatmap beatmap; - using (var stream = new LineBufferedReader(reader.GetStream(mapName))) - beatmap = Decoder.GetDecoder(stream).Decode(stream); - - return new BeatmapSetInfo - { - OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID, - Metadata = beatmap.Metadata, - DateAdded = DateTimeOffset.UtcNow - }; } - - /// - /// Create all required s for the provided archive. - /// - private List createBeatmapDifficulties(List files) - { - var beatmapInfos = new List(); - - foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) - { - using (var raw = Files.Store.GetStream(file.FileInfo.GetStoragePath())) - using (var ms = new MemoryStream()) // we need a memory stream so we can seek - using (var sr = new LineBufferedReader(ms)) - { - raw.CopyTo(ms); - ms.Position = 0; - - var decoder = Decoder.GetDecoder(sr); - IBeatmap beatmap = decoder.Decode(sr); - - string hash = ms.ComputeSHA2Hash(); - - if (beatmapInfos.Any(b => b.Hash == hash)) - continue; - - beatmap.BeatmapInfo.Path = file.Filename; - beatmap.BeatmapInfo.Hash = hash; - beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); - - var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - beatmap.BeatmapInfo.Ruleset = ruleset; - - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.StarRating = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; - beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); - - beatmapInfos.Add(beatmap.BeatmapInfo); - } - } - - return beatmapInfos; - } - - private double calculateLength(IBeatmap b) - { - if (!b.HitObjects.Any()) - return 0; - - var lastObject = b.HitObjects.Last(); - - //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). - double endTime = lastObject.GetEndTime(); - double startTime = b.HitObjects.First().StartTime; - - return endTime - startTime; - } - - /// - /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. - /// - private class DummyConversionBeatmap : WorkingBeatmap - { - private readonly IBeatmap beatmap; - - public DummyConversionBeatmap(IBeatmap beatmap) - : base(beatmap.BeatmapInfo, null) - { - this.beatmap = beatmap; - } - - protected override IBeatmap GetBeatmap() => beatmap; - protected override Texture GetBackground() => null; - protected override Track GetBeatmapTrack() => null; - protected internal override ISkin GetSkin() => null; - public override Stream GetStream(string storagePath) => null; - } - } - - /// - /// The level of detail to include in database results. - /// - public enum IncludedDetails - { - /// - /// Only include beatmap difficulties and set level metadata. - /// - Minimal, - - /// - /// Include all difficulties, rulesets, difficulty metadata but no files. - /// - AllButFiles, - - /// - /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap. - /// - AllButRuleset, - - /// - /// Include everything. - /// - All } } diff --git a/osu.Game/Beatmaps/EFBeatmapSetInfo.cs b/osu.Game/Beatmaps/EFBeatmapSetInfo.cs index c0310f5a76..10910d9817 100644 --- a/osu.Game/Beatmaps/EFBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/EFBeatmapSetInfo.cs @@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps public EFBeatmapMetadata Metadata { get; set; } [NotNull] - public List Beatmaps { get; } = new List(); + public List Beatmaps { get; } = new List(); public BeatmapOnlineStatus Status { get; set; } = BeatmapOnlineStatus.None; @@ -95,7 +95,7 @@ namespace osu.Game.Beatmaps #region Implementation of IBeatmapSetInfo - IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata ?? Beatmaps.FirstOrDefault()?.Metadata ?? new BeatmapMetadata(); + IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata ?? Beatmaps.FirstOrDefault()?.Metadata ?? new EFBeatmapMetadata(); IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; IEnumerable IHasNamedFiles.Files => Files; diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs index 8c915e2872..dc8201a402 100644 --- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo.Ruleset = rulesetProvider(beatmap.BeatmapInfo.RulesetID).RulesetInfo; if (beatmapId.HasValue) - beatmap.BeatmapInfo.OnlineID = beatmapId; + beatmap.BeatmapInfo.OnlineID = beatmapId.Value; } private static Beatmap readFromFile(string filename) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index e5db9d045a..5c61d302c2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Beatmaps.Formats @@ -141,7 +142,8 @@ namespace osu.Game.Beatmaps.Formats break; case @"Mode": - beatmap.BeatmapInfo.RulesetID = Parsing.ParseInt(pair.Value); + // TODO: ha ha ha. + beatmap.BeatmapInfo.Ruleset = new RulesetInfo(Parsing.ParseInt(pair.Value), "some ruleset", "wangs", true /* probably not */); switch (beatmap.BeatmapInfo.RulesetID) { diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs index 1fb5a42630..cd9e396d13 100644 --- a/osu.Game/Database/ImportTask.cs +++ b/osu.Game/Database/ImportTask.cs @@ -5,13 +5,14 @@ using System.IO; using osu.Game.IO.Archives; +using osu.Game.Stores; using osu.Game.Utils; using SharpCompress.Common; namespace osu.Game.Database { /// - /// An encapsulated import task to be imported to an . + /// An encapsulated import task to be imported to an . /// public class ImportTask { diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 3346c6d97d..310f4946f3 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays beatmaps.ItemUpdated += beatmapUpdated; beatmaps.ItemRemoved += beatmapRemoved; - beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next())); + beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(true).OrderBy(_ => RNG.Next())); // Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now. // They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load(). diff --git a/osu.Game/Scoring/LegacyDatabasedScore.cs b/osu.Game/Scoring/LegacyDatabasedScore.cs index 6a8e5f8930..ac444c1bf3 100644 --- a/osu.Game/Scoring/LegacyDatabasedScore.cs +++ b/osu.Game/Scoring/LegacyDatabasedScore.cs @@ -6,14 +6,14 @@ using System.Linq; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Extensions; +using osu.Game.Rulesets; using osu.Game.Scoring.Legacy; -using osu.Game.Stores; namespace osu.Game.Scoring { public class LegacyDatabasedScore : Score { - public LegacyDatabasedScore(ScoreInfo score, RealmRulesetStore rulesets, BeatmapManager beatmaps, IResourceStore store) + public LegacyDatabasedScore(ScoreInfo score, RulesetStore rulesets, BeatmapManager beatmaps, IResourceStore store) { ScoreInfo = score; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 21339acd22..20b6ba7a93 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -80,17 +80,9 @@ namespace osu.Game.Scoring public double? PP { get; set; } - public RealmBeatmap Beatmap { get; set; } = null!; + public BeatmapInfo Beatmap { get; set; } = null!; - public BeatmapInfo BeatmapInfo - { - get => new BeatmapInfo(); - // .. todo - // ReSharper disable once ValueParameterNotUsed - set => Beatmap = new RealmBeatmap(new RealmRuleset("osu", "osu!", "wangs", 0), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - } - - public RealmRuleset Ruleset { get; set; } = null!; + public RulesetInfo Ruleset { get; set; } = null!; [Ignored] public Dictionary Statistics @@ -132,7 +124,13 @@ namespace osu.Game.Scoring private Mod[]? mods; - public int BeatmapInfoID => BeatmapInfo.ID; + public Guid BeatmapInfoID => Beatmap.ID; + + public BeatmapInfo BeatmapInfo + { + get => Beatmap; + set => Beatmap = value; + } public int UserID => RealmUser.OnlineID; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index d094e1f45c..0a1b616422 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -20,7 +20,6 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Stores; namespace osu.Game.Scoring { diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index dd0ac79873..81f80df3ad 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -11,6 +11,7 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.IO.Archives; +using osu.Game.Rulesets; using osu.Game.Scoring.Legacy; using osu.Game.Stores; using Realms; @@ -25,10 +26,10 @@ namespace osu.Game.Scoring protected override string[] HashableFileTypes => new[] { ".osr" }; - private readonly RealmRulesetStore rulesets; + private readonly RulesetStore rulesets; private readonly Func beatmaps; - public ScoreModelManager(RealmRulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory) + public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory) : base(storage, contextFactory) { this.rulesets = rulesets; diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 948e3a7d88..94c48f5251 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Menu // if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection. if (!MenuMusic.Value) { - var sets = beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal); + var sets = beatmaps.GetAllUsableBeatmapSets(); if (sets.Count > 0) { @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Menu bool loadThemedIntro() { - setInfo = beatmaps.QueryBeatmapSets(b => b.Hash == BeatmapHash, IncludedDetails.AllButRuleset).FirstOrDefault(); + setInfo = beatmaps.QueryBeatmapSets(b => b.Hash == BeatmapHash).FirstOrDefault(); if (setInfo == null) return false; diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index afebc728b4..2ec6c38287 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { - if (Score.BeatmapInfo.OnlineID == null || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + if (Score.BeatmapInfo.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending) return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 7e292e2de7..f8d2c3cb9b 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.Select loadBeatmapSets(GetLoadableBeatmaps()); } - protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.AllButFiles); + protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 619b1e0fd0..618c5cf5ec 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -61,7 +61,11 @@ namespace osu.Game.Screens.Select.Carousel [BackgroundDependencyLoader(true)] private void load(BeatmapSetOverlay beatmapOverlay) { - restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore); + restoreHiddenRequested = s => + { + foreach (var b in s.Beatmaps) + manager.Restore(b); + }; if (beatmapOverlay != null) viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; @@ -214,8 +218,8 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value == CarouselItemState.NotSelected) items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected)); - if (beatmapSet.OnlineID != null && viewDetails != null) - items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID.Value))); + if (beatmapSet.OnlineID > 0 && viewDetails != null) + items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID))); if (collectionManager != null) { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a40e8e2bde..1aab84183a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -295,7 +295,7 @@ namespace osu.Game.Screens.Select Schedule(() => { // if we have no beatmaps, let's prompt the user to import from over a stable install if he has one. - if (!beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.Minimal).Any() && DisplayStableImportPrompt) + if (!beatmaps.GetAllUsableBeatmapSetsEnumerable().Any() && DisplayStableImportPrompt) { dialogOverlay.Push(new ImportFromStablePopup(() => { diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index de92da394d..2c73bd22eb 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -34,7 +34,7 @@ namespace osu.Game.Stores /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapImporter : RealmArchiveModelImporter, IDisposable + public abstract class BeatmapImporter : RealmArchiveModelManager, IDisposable { public override IEnumerable HandledExtensions => new[] { ".osz" }; @@ -45,7 +45,7 @@ namespace osu.Game.Stores private readonly BeatmapOnlineLookupQueue? onlineLookupQueue; - public BeatmapImporter(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) + protected BeatmapImporter(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) : base(storage, contextFactory) { this.onlineLookupQueue = onlineLookupQueue; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 8a31e4576a..d21813d97f 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -10,8 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; -using osu.Game.IO; using osu.Game.Screens.Play; +using osu.Game.Stores; namespace osu.Game.Storyboards.Drawables { @@ -76,7 +76,7 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken, GameHost host) + private void load(RealmFileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken, GameHost host) { if (clock != null) Clock = clock; diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 99944bcf6d..3b4547cb49 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -32,14 +32,12 @@ namespace osu.Game.Tests.Beatmaps HitObjects = baseBeatmap.HitObjects; BeatmapInfo.Ruleset = ruleset; - BeatmapInfo.RulesetID = ruleset.ID ?? 0; BeatmapInfo.Length = 75000; BeatmapInfo.OnlineInfo = new APIBeatmap(); BeatmapInfo.OnlineID = Interlocked.Increment(ref onlineBeatmapID); Debug.Assert(BeatmapInfo.BeatmapSet != null); - BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; BeatmapInfo.BeatmapSet.Beatmaps.Add(BeatmapInfo); BeatmapInfo.BeatmapSet.OnlineID = Interlocked.Increment(ref onlineSetID); } diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index a2f567f3b0..570e4c26b3 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -122,9 +122,9 @@ namespace osu.Game.Tests.Visual this.testBeatmap = testBeatmap; } - protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) { - return new TestBeatmapModelManager(storage, contextFactory, rulesets, api, host); + return new TestBeatmapModelManager(storage, contextFactory, rulesets, onlineLookupQueue); } protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) @@ -148,8 +148,8 @@ namespace osu.Game.Tests.Visual internal class TestBeatmapModelManager : BeatmapModelManager { - public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) - : base(storage, databaseContextFactory, rulesetStore, gameHost) + public TestBeatmapModelManager(Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) + : base(databaseContextFactory, storage, beatmapOnlineLookupQueue) { } From d7fe3584cde52011d776d26f1bcb95a651d56896 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 21:00:19 +0900 Subject: [PATCH 037/227] Don't persist `Countdown` to realm for now It's another enum which is a pain to handle, and not actually being consumed anywhere. --- osu.Game/Beatmaps/BeatmapInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 796c2b2e90..fc0d007df4 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -101,6 +101,7 @@ namespace osu.Game.Beatmaps public double TimelineZoom { get; set; } + [Ignored] public CountdownType Countdown { get; set; } = CountdownType.Normal; /// From 8d943b57095cc17f172ba83086b50d05c86bed89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 00:31:35 +0900 Subject: [PATCH 038/227] Fix many shortcomings and compatibility issues with EF classes post-rename --- osu.Game/Beatmaps/BeatmapSetFileInfo.cs | 4 +++- osu.Game/Beatmaps/EFBeatmapDifficulty.cs | 2 ++ osu.Game/Beatmaps/EFBeatmapInfo.cs | 3 ++- osu.Game/Beatmaps/EFBeatmapMetadata.cs | 1 + osu.Game/Beatmaps/EFBeatmapSetInfo.cs | 2 ++ osu.Game/Database/OsuDbContext.cs | 2 +- osu.Game/Rulesets/EFRulesetInfo.cs | 1 + osu.Game/Scoring/EFScoreInfo.cs | 29 +++++++++++++----------- osu.Game/Scoring/ScoreFileInfo.cs | 4 ++++ osu.Game/Scoring/ScoreInfo.cs | 9 -------- 10 files changed, 32 insertions(+), 25 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs index 29dcf4d6aa..3d41f59b3d 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -15,6 +15,8 @@ namespace osu.Game.Beatmaps public int BeatmapSetInfoID { get; set; } + public EFBeatmapSetInfo BeatmapSetInfo { get; set; } + public int FileInfoID { get; set; } public FileInfo FileInfo { get; set; } @@ -22,6 +24,6 @@ namespace osu.Game.Beatmaps [Required] public string Filename { get; set; } - public IFileInfo File => FileInfo; + IFileInfo INamedFileUsage.File => FileInfo; } } diff --git a/osu.Game/Beatmaps/EFBeatmapDifficulty.cs b/osu.Game/Beatmaps/EFBeatmapDifficulty.cs index 843c6ac6bf..38371d3b38 100644 --- a/osu.Game/Beatmaps/EFBeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/EFBeatmapDifficulty.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.ComponentModel.DataAnnotations.Schema; using osu.Game.Database; namespace osu.Game.Beatmaps { + [Table(@"BeatmapDifficulty")] public class EFBeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo { /// diff --git a/osu.Game/Beatmaps/EFBeatmapInfo.cs b/osu.Game/Beatmaps/EFBeatmapInfo.cs index 2336eeb456..8257d0cf7d 100644 --- a/osu.Game/Beatmaps/EFBeatmapInfo.cs +++ b/osu.Game/Beatmaps/EFBeatmapInfo.cs @@ -16,6 +16,7 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] + [Table(@"BeatmapInfo")] public class EFBeatmapInfo : IEquatable, IHasPrimaryKey, IBeatmapInfo { public int ID { get; set; } @@ -124,7 +125,7 @@ namespace osu.Game.Beatmaps /// /// Currently only populated for beatmap deletion. Use to query scores. /// - public List Scores { get; set; } + public List Scores { get; set; } [JsonIgnore] public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarRating); diff --git a/osu.Game/Beatmaps/EFBeatmapMetadata.cs b/osu.Game/Beatmaps/EFBeatmapMetadata.cs index 6c192088dc..7c27863a7f 100644 --- a/osu.Game/Beatmaps/EFBeatmapMetadata.cs +++ b/osu.Game/Beatmaps/EFBeatmapMetadata.cs @@ -16,6 +16,7 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] + [Table(@"BeatmapMetadata")] public class EFBeatmapMetadata : IEquatable, IHasPrimaryKey, IBeatmapMetadataInfo { public int ID { get; set; } diff --git a/osu.Game/Beatmaps/EFBeatmapSetInfo.cs b/osu.Game/Beatmaps/EFBeatmapSetInfo.cs index 10910d9817..12235abce0 100644 --- a/osu.Game/Beatmaps/EFBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/EFBeatmapSetInfo.cs @@ -14,6 +14,8 @@ using osu.Game.Extensions; namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] + [Serializable] + [Table(@"BeatmapSetInfo")] public class EFBeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable, IBeatmapSetInfo { public int ID { get; set; } diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index fb83592c01..441b090a6e 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -147,7 +147,7 @@ namespace osu.Game.Database modelBuilder.Entity().HasOne(b => b.BaseDifficulty); - modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.OnlineID).IsUnique(); } private class OsuDbLoggerFactory : ILoggerFactory diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index a7070ad2b6..473b7c657e 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -10,6 +10,7 @@ using osu.Framework.Testing; namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] + [Table(@"RulesetInfo")] public sealed class EFRulesetInfo : IEquatable, IRulesetInfo { public int? ID { get; set; } diff --git a/osu.Game/Scoring/EFScoreInfo.cs b/osu.Game/Scoring/EFScoreInfo.cs index 84b41e40ef..1dd4e3b6b3 100644 --- a/osu.Game/Scoring/EFScoreInfo.cs +++ b/osu.Game/Scoring/EFScoreInfo.cs @@ -19,6 +19,7 @@ using osu.Game.Utils; namespace osu.Game.Scoring { + [Table(@"ScoreInfo")] public class EFScoreInfo : IScoreInfo, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable, IDeepCloneable { public int ID { get; set; } @@ -45,7 +46,7 @@ namespace osu.Game.Scoring [NotMapped] public bool Passed { get; set; } = true; - public RulesetInfo Ruleset { get; set; } + public EFRulesetInfo Ruleset { get; set; } private APIMod[] localAPIMods; @@ -135,9 +136,17 @@ namespace osu.Game.Scoring public int BeatmapInfoID { get; set; } [Column("Beatmap")] - public BeatmapInfo BeatmapInfo { get; set; } + public EFBeatmapInfo BeatmapInfo { get; set; } - public long? OnlineScoreID { get; set; } + private long? onlineID; + + [JsonProperty("id")] + [Column("OnlineScoreID")] + public long? OnlineID + { + get => onlineID; + set => onlineID = value > 0 ? value : null; + } public DateTimeOffset Date { get; set; } @@ -232,24 +241,18 @@ namespace osu.Game.Scoring public bool Equals(EFScoreInfo other) { - if (other == null) - return false; + if (ReferenceEquals(this, other)) return true; + if (other == null) return false; if (ID != 0 && other.ID != 0) return ID == other.ID; - if (OnlineScoreID.HasValue && other.OnlineScoreID.HasValue) - return OnlineScoreID == other.OnlineScoreID; - - if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) - return Hash == other.Hash; - - return ReferenceEquals(this, other); + return false; } #region Implementation of IHasOnlineID - public long OnlineID => OnlineScoreID ?? -1; + long IHasOnlineID.OnlineID => OnlineID ?? -1; #endregion diff --git a/osu.Game/Scoring/ScoreFileInfo.cs b/osu.Game/Scoring/ScoreFileInfo.cs index 4c88cfa021..8acc98eff6 100644 --- a/osu.Game/Scoring/ScoreFileInfo.cs +++ b/osu.Game/Scoring/ScoreFileInfo.cs @@ -13,6 +13,10 @@ namespace osu.Game.Scoring public bool IsManaged => ID > 0; + public int ScoreInfoID { get; set; } + + public EFScoreInfo ScoreInfo { get; set; } + public int FileInfoID { get; set; } public FileInfo FileInfo { get; set; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 20b6ba7a93..65c31951b8 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; using osu.Framework.Localisation; @@ -215,14 +214,6 @@ namespace osu.Game.Scoring } } - // Used for database serialisation/deserialisation. - [Column("Mods")] - public string ModsJson - { - get => JsonConvert.SerializeObject(APIMods); - set => APIMods = JsonConvert.DeserializeObject(value) ?? Array.Empty(); - } - public IEnumerable GetStatisticsForDisplay() { foreach (var r in Ruleset.CreateInstance().GetHitResults()) From 3ecd535f6e7f49fb2484109d0d44377810a7e9bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 00:33:00 +0900 Subject: [PATCH 039/227] Add back missing `IRulesetStore` cache --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 89e3fe5742..ac04be532a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -222,6 +222,7 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); + dependencies.CacheAs(RulesetStore); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realmFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); From c4a9211179ed93cd287e67aa763e14dfae8ae54a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 14:26:38 +0900 Subject: [PATCH 040/227] Apply NRT to `BeatmapManager` and move `Hide`/`Restore` methods across --- osu.Game/Beatmaps/BeatmapManager.cs | 94 +++++++++++++----------- osu.Game/Beatmaps/BeatmapModelManager.cs | 45 ------------ 2 files changed, 51 insertions(+), 88 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 45ae69e4ba..efe9c65f9f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions; @@ -25,6 +24,8 @@ using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Stores; +#nullable enable + namespace osu.Game.Beatmaps { /// @@ -38,10 +39,13 @@ namespace osu.Game.Beatmaps private readonly BeatmapModelManager beatmapModelManager; private readonly WorkingBeatmapCache workingBeatmapCache; - private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue; + private readonly BeatmapOnlineLookupQueue? onlineBeatmapLookupQueue; - public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) + private readonly RealmContextFactory contextFactory; + + public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false) { + this.contextFactory = contextFactory; if (performOnlineLookups) onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); @@ -56,12 +60,12 @@ namespace osu.Game.Beatmaps beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; } - protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) + protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap? defaultBeatmap, GameHost? host) { return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host); } - protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, [CanBeNull] BeatmapOnlineLookupQueue onlineLookupQueue) => + protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) => new BeatmapModelManager(contextFactory, storage, onlineLookupQueue); /// @@ -103,19 +107,43 @@ namespace osu.Game.Beatmaps /// /// Fired when a single difficulty has been hidden. /// - public event Action BeatmapHidden - { - add => beatmapModelManager.BeatmapHidden += value; - remove => beatmapModelManager.BeatmapHidden -= value; - } + public event Action? BeatmapHidden; /// /// Fired when a single difficulty has been restored. /// - public event Action BeatmapRestored + public event Action? BeatmapRestored; + + /// + /// Delete a beatmap difficulty. + /// + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmapInfo) { - add => beatmapModelManager.BeatmapRestored += value; - remove => beatmapModelManager.BeatmapRestored -= value; + using (var realm = contextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + beatmapInfo.Hidden = true; + transaction.Commit(); + + BeatmapHidden?.Invoke(beatmapInfo); + } + } + + /// + /// Restore a beatmap difficulty. + /// + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmapInfo) + { + using (var realm = contextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + beatmapInfo.Hidden = false; + transaction.Commit(); + + BeatmapRestored?.Invoke(beatmapInfo); + } } /// @@ -124,7 +152,7 @@ namespace osu.Game.Beatmaps /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) => + public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin? beatmapSkin = null) => beatmapModelManager.Save(info, beatmapContent, beatmapSkin); /// @@ -152,7 +180,7 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmapModelManager.QueryBeatmapSet(query); + public BeatmapSetInfo? QueryBeatmapSet(Expression> query) => beatmapModelManager.QueryBeatmapSet(query); /// /// Perform a lookup query on available s. @@ -166,7 +194,7 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapInfo QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query); + public BeatmapInfo? QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. @@ -181,18 +209,6 @@ namespace osu.Game.Beatmaps set => beatmapModelManager.PostNotification = value; } - /// - /// Delete a beatmap difficulty. - /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmapInfo) => beatmapModelManager.Hide(beatmapInfo); - - /// - /// Restore a beatmap difficulty. - /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmapInfo) => beatmapModelManager.Restore(beatmapInfo); - #endregion #region Implementation of IModelManager @@ -202,17 +218,9 @@ namespace osu.Game.Beatmaps return beatmapModelManager.IsAvailableLocally(model); } - public event Action ItemUpdated - { - add => beatmapModelManager.ItemUpdated += value; - remove => beatmapModelManager.ItemUpdated -= value; - } + public event Action? ItemUpdated; - public event Action ItemRemoved - { - add => beatmapModelManager.ItemRemoved += value; - remove => beatmapModelManager.ItemRemoved -= value; - } + public event Action? ItemRemoved; public void Update(BeatmapSetInfo item) { @@ -258,17 +266,17 @@ namespace osu.Game.Beatmaps return beatmapModelManager.Import(notification, tasks); } - public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(task, lowPriority, cancellationToken); } - public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(archive, lowPriority, cancellationToken); } - public Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task?> Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); } @@ -279,7 +287,7 @@ namespace osu.Game.Beatmaps #region Implementation of IWorkingBeatmapCache - public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); + public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo); void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo); @@ -316,7 +324,7 @@ namespace osu.Game.Beatmaps #region Implementation of IPostImports - public Action>> PostImport + public Action>>? PostImport { set => beatmapModelManager.PostImport = value; } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 100189bf09..1d1aa20a5b 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -21,22 +21,9 @@ using osu.Game.Stores; namespace osu.Game.Beatmaps { - /// - /// Handles ef-core storage of beatmaps. - /// [ExcludeFromDynamicCompile] public class BeatmapModelManager : BeatmapImporter { - /// - /// Fired when a single difficulty has been hidden. - /// - public event Action? BeatmapHidden; - - /// - /// Fired when a single difficulty has been restored. - /// - public event Action? BeatmapRestored; - /// /// The game working beatmap cache, used to invalidate entries on changes. /// @@ -53,38 +40,6 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - /// - /// Delete a beatmap difficulty. - /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmapInfo) - { - using (var realm = ContextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) - { - beatmapInfo.Hidden = true; - transaction.Commit(); - - BeatmapHidden?.Invoke(beatmapInfo); - } - } - - /// - /// Restore a beatmap difficulty. - /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmapInfo) - { - using (var realm = ContextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) - { - beatmapInfo.Hidden = false; - transaction.Commit(); - - BeatmapRestored?.Invoke(beatmapInfo); - } - } - /// /// Saves an file against a given . /// From abd72c496ba470888ef84b7055fd6078967314a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Nov 2021 17:59:23 +0900 Subject: [PATCH 041/227] "Update" `MusicController` --- osu.Game/Overlays/MusicController.cs | 46 +++++++++++++++++++--------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 310f4946f3..58707c3321 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -12,10 +12,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; -using osu.Framework.Utils; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets.Mods; +using Realms; namespace osu.Game.Overlays { @@ -24,6 +25,8 @@ namespace osu.Game.Overlays /// public class MusicController : CompositeDrawable { + private IDisposable beatmapSubscription; + [Resolved] private BeatmapManager beatmaps { get; set; } @@ -65,13 +68,14 @@ namespace osu.Game.Overlays [NotNull] public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000)); - [BackgroundDependencyLoader] - private void load() - { - beatmaps.ItemUpdated += beatmapUpdated; - beatmaps.ItemRemoved += beatmapRemoved; + [Resolved] + private RealmContextFactory realmFactory { get; set; } - beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(true).OrderBy(_ => RNG.Next())); + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmapSubscription = realmFactory.Context.All().QueryAsyncWithNotifications(beatmapsChanged); // Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now. // They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load(). @@ -79,6 +83,24 @@ namespace osu.Game.Overlays mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } + private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + { + if (changes == null) + { + beatmapSets.AddRange(sender); + return; + } + + // beatmaps.ItemUpdated += set => Schedule(() => + // { + // beatmapSets.Remove(set); + // beatmapSets.Add(set); + // }); + // beatmaps.ItemRemoved += set => Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID)); + // + // beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next())); + } + /// /// Forcefully reload the current 's track from disk. /// @@ -111,7 +133,7 @@ namespace osu.Game.Overlays beatmapSets.Add(set); }); - private void beatmapRemoved(BeatmapSetInfo set) => Schedule(() => beatmapSets.RemoveAll(s => s.Equals(set))); + private void beatmapRemoved(BeatmapSetInfo set) => Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID)); private ScheduledDelegate seekDelegate; @@ -259,7 +281,7 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Next; - var playable = BeatmapSets.SkipWhile(i => !i.Equals(current.BeatmapSetInfo)).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault(); + var playable = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault(); if (playable != null) { @@ -429,11 +451,7 @@ namespace osu.Game.Overlays { base.Dispose(isDisposing); - if (beatmaps != null) - { - beatmaps.ItemUpdated -= beatmapUpdated; - beatmaps.ItemRemoved -= beatmapRemoved; - } + beatmapSubscription?.Dispose(); } } From 8696f82627ea226831f1c245487a5ef1f2b3ef63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 14:36:11 +0900 Subject: [PATCH 042/227] Fix intro screen Fix things --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index efe9c65f9f..1d39873016 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -173,7 +173,7 @@ namespace osu.Game.Beatmaps /// /// The query. /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query) => beatmapModelManager.QueryBeatmapSets(query); + public ILive> QueryBeatmapSets(Expression> query) => beatmapModelManager.QueryBeatmapSets(query).ToLive(contextFactory); /// /// Perform a lookup query on available s. diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 94c48f5251..aa3fe9fd88 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Menu if (sets.Count > 0) { setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo?.Beatmaps[0]); } } @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Menu // this could happen if a user has nuked their files store. for now, reimport to repair this. var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely(); - import.PerformWrite(b => + import?.PerformWrite(b => { b.Protected = true; beatmaps.Update(b); From 3152d2d8a02411fb690965065c03f12ca3f9f1fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Nov 2021 17:41:42 +0900 Subject: [PATCH 043/227] "Update" `BeatmapCarousel` --- .../SongSelect/TestSceneBeatmapCarousel.cs | 2 - osu.Game/Screens/Select/BeatmapCarousel.cs | 67 ++++++++++++++----- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 28f5c3ff60..b1414d5896 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -873,8 +873,6 @@ namespace osu.Game.Tests.Visual.SongSelect } } } - - protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty(); } } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f8d2c3cb9b..94e7dc9241 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -18,12 +18,14 @@ using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; using osuTK; using osuTK.Input; +using Realms; namespace osu.Game.Screens.Select { @@ -172,17 +174,57 @@ namespace osu.Game.Screens.Select RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.TriggerChange(); - - beatmaps.ItemUpdated += beatmapUpdated; - beatmaps.ItemRemoved += beatmapRemoved; - beatmaps.BeatmapHidden += beatmapHidden; - beatmaps.BeatmapRestored += beatmapRestored; - - if (!beatmapSets.Any()) - loadBeatmapSets(GetLoadableBeatmaps()); } - protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); + [Resolved] + private RealmContextFactory realmFactory { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + realmFactory.Context.All().Where(s => !s.DeletePending).QueryAsyncWithNotifications(beatmapSetsChanged); + realmFactory.Context.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); + } + + private void beatmapRemoved(BeatmapSetInfo item) => RemoveBeatmapSet(item); + private void beatmapUpdated(BeatmapSetInfo item) => UpdateBeatmapSet(item); + + private void beatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + { + if (changes == null) + { + // initial load + loadBeatmapSets(sender); + return; + } + + foreach (int i in changes.NewModifiedIndices) + UpdateBeatmapSet(sender[i]); + + // moves also appear as deletes / inserts but aren't important to us. + if (!changes.Moves.Any()) + { + foreach (int i in changes.InsertedIndices) + UpdateBeatmapSet(sender[i]); + + // TODO: This can not work, as we recently found out https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. + foreach (int i in changes.DeletedIndices) + RemoveBeatmapSet(sender[i]); + } + } + + private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + { + // we only care about actual changes in hidden status. + if (changes == null) + return; + + // TODO: we can probably handle hidden items at a per-panel level (ie. start a realm subscription from there)? + // might be cleaner than handling via reconstruction of the whole set's panels. + foreach (int i in changes.NewModifiedIndices) + UpdateBeatmapSet(sender[i].BeatmapSet); + } public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { @@ -617,11 +659,6 @@ namespace osu.Game.Screens.Select return (firstIndex, lastIndex); } - private void beatmapRemoved(BeatmapSetInfo item) => RemoveBeatmapSet(item); - private void beatmapUpdated(BeatmapSetInfo item) => UpdateBeatmapSet(item); - private void beatmapRestored(BeatmapInfo b) => UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); - private void beatmapHidden(BeatmapInfo b) => UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); - private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet) { if (beatmapSet.Beatmaps.All(b => b.Hidden)) @@ -888,8 +925,6 @@ namespace osu.Game.Screens.Select { beatmaps.ItemUpdated -= beatmapUpdated; beatmaps.ItemRemoved -= beatmapRemoved; - beatmaps.BeatmapHidden -= beatmapHidden; - beatmaps.BeatmapRestored -= beatmapRestored; } } } From 1d536fd0bc6bed1575503f7a5f7636cbc0079bc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 15:03:06 +0900 Subject: [PATCH 044/227] Start introducing `ILive` --- osu.Game/Beatmaps/BeatmapManager.cs | 4 +- osu.Game/Beatmaps/BeatmapModelManager.cs | 4 +- osu.Game/OsuGame.cs | 45 ++++++++++--------- osu.Game/Overlays/MusicController.cs | 1 - .../Sections/Maintenance/GeneralSettings.cs | 3 +- osu.Game/Screens/Menu/IntroScreen.cs | 7 +-- 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 1d39873016..7fb0729967 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -173,14 +173,14 @@ namespace osu.Game.Beatmaps /// /// The query. /// Results from the provided query. - public ILive> QueryBeatmapSets(Expression> query) => beatmapModelManager.QueryBeatmapSets(query).ToLive(contextFactory); + public List> QueryBeatmapSets(Expression> query) => beatmapModelManager.QueryBeatmapSets(query).ToLive(); /// /// Perform a lookup query on available s. /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo? QueryBeatmapSet(Expression> query) => beatmapModelManager.QueryBeatmapSet(query); + public ILive? QueryBeatmapSet(Expression> query) => beatmapModelManager.QueryBeatmapSet(query); /// /// Perform a lookup query on available s. diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 1d1aa20a5b..839d3df159 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -104,10 +104,10 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo? QueryBeatmapSet(Expression> query) + public ILive? QueryBeatmapSet(Expression> query) { using (var context = ContextFactory.CreateContext()) - return context.All().FirstOrDefault(query); // TODO: ?.ToLive(); + return context.All().FirstOrDefault(query)?.ToLive(); } /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 49893a6d36..8d1eccb7c1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -437,7 +437,7 @@ namespace osu.Game /// public void PresentBeatmap(IBeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { - BeatmapSetInfo databasedSet = null; + ILive databasedSet = null; if (beatmap.OnlineID > 0) databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID); @@ -453,27 +453,30 @@ namespace osu.Game PerformFromScreen(screen => { - // Find beatmaps that match our predicate. - var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); - - // Use all beatmaps if predicate matched nothing - if (beatmaps.Count == 0) - beatmaps = databasedSet.Beatmaps.ToList(); - - // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection. - var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps) - ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) - ?? beatmaps.First(); - - if (screen is IHandlePresentBeatmap presentableScreen) + databasedSet.PerformRead(set => { - presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset); - } - else - { - Ruleset.Value = selection.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); - } + // Find beatmaps that match our predicate. + var beatmaps = set.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); + + // Use all beatmaps if predicate matched nothing + if (beatmaps.Count == 0) + beatmaps = set.Beatmaps.ToList(); + + // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection. + var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps) + ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) + ?? beatmaps.First(); + + if (screen is IHandlePresentBeatmap presentableScreen) + { + presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset); + } + else + { + Ruleset.Value = selection.Ruleset; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); + } + }); }, validScreens: new[] { typeof(SongSelect), typeof(IHandlePresentBeatmap) }); } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 58707c3321..8c115c2c94 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -88,7 +88,6 @@ namespace osu.Game.Overlays if (changes == null) { beatmapSets.AddRange(sender); - return; } // beatmaps.ItemUpdated += set => Schedule(() => diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 51e4d61d2e..93884812fe 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -160,7 +160,8 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { undeleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); + // TODO: reimplement similar to SkinManager? + // Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); } }, }); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index aa3fe9fd88..bb8ddb8cd6 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -15,6 +15,7 @@ using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; @@ -90,7 +91,7 @@ namespace osu.Game.Screens.Menu MenuMusic = config.GetBindable(OsuSetting.MenuMusic); seeya = audio.Samples.Get(SeeyaSampleName); - BeatmapSetInfo setInfo = null; + ILive setInfo = null; // if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection. if (!MenuMusic.Value) @@ -100,7 +101,7 @@ namespace osu.Game.Screens.Menu if (sets.Count > 0) { setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo?.Beatmaps[0]); + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo?.PerformRead(s => s.Beatmaps[0].ToLive())); } } @@ -130,7 +131,7 @@ namespace osu.Game.Screens.Menu if (setInfo == null) return false; - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.PerformRead(s => s.Beatmaps[0].ToLive())); return UsingThemedIntro = initialBeatmap != null; } From b91f3098793c19a09554abb5b96e17ae8212a4f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 15:24:26 +0900 Subject: [PATCH 045/227] Inline query methods from `BeatmapModelManager` to `BeatmapManager` where possible --- osu.Game/Beatmaps/BeatmapManager.cs | 60 ++++++++++++++++++------ osu.Game/Beatmaps/BeatmapModelManager.cs | 58 ----------------------- 2 files changed, 45 insertions(+), 73 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 7fb0729967..22871bd332 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -146,48 +146,60 @@ namespace osu.Game.Beatmaps } } - /// - /// Saves an file against a given . - /// - /// The to save the content against. The file referenced by will be replaced. - /// The content to write. - /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin? beatmapSkin = null) => - beatmapModelManager.Save(info, beatmapContent, beatmapSkin); - /// /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets(bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includeProtected); + public List GetAllUsableBeatmapSets(bool includeProtected = false) + { + using (var context = contextFactory.CreateContext()) + return context.All().Where(b => !b.DeletePending && (includeProtected || !b.Protected)).ToList(); + } /// - /// Returns a list of all usable s. Note that files are not populated. + /// Returns a list of all usable s. Note that files are not populated. TODO: do we still need this? or rather, should we have the non-enumerable version in the first place? /// /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. /// A list of available . - public IEnumerable GetAllUsableBeatmapSetsEnumerable(bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includeProtected); + public IEnumerable GetAllUsableBeatmapSetsEnumerable(bool includeProtected = false) => GetAllUsableBeatmapSets(includeProtected); /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. - public List> QueryBeatmapSets(Expression> query) => beatmapModelManager.QueryBeatmapSets(query).ToLive(); + public List> QueryBeatmapSets(Expression> query) + { + using (var context = contextFactory.CreateContext()) + { + return context.All() + .Where(b => !b.DeletePending) + .Where(query) + .ToLive(); + } + } /// /// Perform a lookup query on available s. /// /// The query. /// The first result for the provided query, or null if no results were found. - public ILive? QueryBeatmapSet(Expression> query) => beatmapModelManager.QueryBeatmapSet(query); + public ILive? QueryBeatmapSet(Expression> query) + { + using (var context = contextFactory.CreateContext()) + return context.All().FirstOrDefault(query)?.ToLive(); + } /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. - public IQueryable QueryBeatmaps(Expression> query) => beatmapModelManager.QueryBeatmaps(query); + public IQueryable QueryBeatmaps(Expression> query) + { + using (var context = contextFactory.CreateContext()) + return context.All().Where(query); + } /// /// Perform a lookup query on available s. @@ -196,6 +208,15 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query); + /// + /// Saves an file against a given . + /// + /// The to save the content against. The file referenced by will be replaced. + /// The content to write. + /// The beatmap content to write, null if to be omitted. + public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin? beatmapSkin = null) => + beatmapModelManager.Save(info, beatmapContent, beatmapSkin); + /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. /// @@ -289,6 +310,15 @@ namespace osu.Game.Beatmaps public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); + public WorkingBeatmap GetWorkingBeatmap(ILive? importedBeatmap) + { + WorkingBeatmap working = workingBeatmapCache.GetWorkingBeatmap(null); + + importedBeatmap?.PerformRead(b => working = workingBeatmapCache.GetWorkingBeatmap(b)); + + return working; + } + void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo); void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 839d3df159..387521ee1d 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -99,50 +99,6 @@ namespace osu.Game.Beatmaps WorkingBeatmapCache?.Invalidate(beatmapInfo); } - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// The first result for the provided query, or null if no results were found. - public ILive? QueryBeatmapSet(Expression> query) - { - using (var context = ContextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(); - } - - /// - /// Returns a list of all usable s. - /// - /// A list of available . - public List GetAllUsableBeatmapSets(bool includeProtected = false) => - GetAllUsableBeatmapSetsEnumerable(includeProtected).ToList(); - - /// - /// Returns a list of all usable s. Note that files are not populated. - /// - /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. - /// A list of available . - public IEnumerable GetAllUsableBeatmapSetsEnumerable(bool includeProtected = false) - { - using (var context = ContextFactory.CreateContext()) - return context.All().Where(b => !b.DeletePending && (includeProtected || !b.Protected)); - } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query) - { - using (var context = ContextFactory.CreateContext()) - { - return context.All() - .Where(b => !b.DeletePending) - .Where(query); - } - } - /// /// Perform a lookup query on available s. /// @@ -153,19 +109,5 @@ namespace osu.Game.Beatmaps using (var context = ContextFactory.CreateContext()) return context.All().FirstOrDefault(query); // TODO: ?.ToLive(); } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// Results from the provided query. - public IQueryable QueryBeatmaps(Expression> query) - { - using (var context = ContextFactory.CreateContext()) - { - return context.All() - .Where(query); - } - } } } From a3276758b8c8c121272b633df3d05b428e455d5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 15:26:48 +0900 Subject: [PATCH 046/227] Remove unnecessary re-query of beatmap set in editor menu construction --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7955464590..3a420f4994 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -776,7 +776,7 @@ namespace osu.Game.Screens.Edit fileMenuItems.Add(new EditorMenuItemSpacer()); - var beatmapSet = beatmapManager.QueryBeatmapSet(bs => bs.ID == Beatmap.Value.BeatmapSetInfo.ID) ?? playableBeatmap.BeatmapInfo.BeatmapSet; + var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet; Debug.Assert(beatmapSet != null); From db05727ec49faf970578237cb1f36b00fa162b0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 20:11:45 +0900 Subject: [PATCH 047/227] Remove unused `includeProtected` parameter --- osu.Game/Beatmaps/BeatmapManager.cs | 15 ++++----------- osu.Game/Screens/Select/SongSelect.cs | 4 ++-- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 22871bd332..706160bfb5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -102,8 +102,6 @@ namespace osu.Game.Beatmaps return GetWorkingBeatmap(imported?.Beatmaps.First()); } - #region Delegation to BeatmapModelManager (methods which previously existed locally). - /// /// Fired when a single difficulty has been hidden. /// @@ -150,19 +148,12 @@ namespace osu.Game.Beatmaps /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets(bool includeProtected = false) + public List GetAllUsableBeatmapSets() { using (var context = contextFactory.CreateContext()) - return context.All().Where(b => !b.DeletePending && (includeProtected || !b.Protected)).ToList(); + return context.All().Where(b => !b.DeletePending).ToList(); } - /// - /// Returns a list of all usable s. Note that files are not populated. TODO: do we still need this? or rather, should we have the non-enumerable version in the first place? - /// - /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. - /// A list of available . - public IEnumerable GetAllUsableBeatmapSetsEnumerable(bool includeProtected = false) => GetAllUsableBeatmapSets(includeProtected); - /// /// Perform a lookup query on available s. /// @@ -201,6 +192,8 @@ namespace osu.Game.Beatmaps return context.All().Where(query); } + #region Delegation to BeatmapModelManager (methods which previously existed locally). + /// /// Perform a lookup query on available s. /// diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 1aab84183a..54a4c3270d 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -295,7 +295,7 @@ namespace osu.Game.Screens.Select Schedule(() => { // if we have no beatmaps, let's prompt the user to import from over a stable install if he has one. - if (!beatmaps.GetAllUsableBeatmapSetsEnumerable().Any() && DisplayStableImportPrompt) + if (!beatmaps.GetAllUsableBeatmapSets().Any() && DisplayStableImportPrompt) { dialogOverlay.Push(new ImportFromStablePopup(() => { @@ -421,7 +421,7 @@ namespace osu.Game.Screens.Select // A selection may not have been possible with filters applied. // There was possibly a ruleset mismatch. This is a case we can help things along by updating the game-wide ruleset to match. - if (e.NewValue.BeatmapInfo.Ruleset != null && !e.NewValue.BeatmapInfo.Ruleset.Equals(decoupledRuleset.Value)) + if (!e.NewValue.BeatmapInfo.Ruleset.Equals(decoupledRuleset.Value)) { Ruleset.Value = e.NewValue.BeatmapInfo.Ruleset; transferRulesetValue(); From 31a3161189f7eb915708fbbeef05a27292f05b0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 18:26:12 +0900 Subject: [PATCH 048/227] Make tests compile again --- .../Beatmaps/IO/BeatmapImportHelper.cs | 87 +++++++++++++++++++ .../TestSceneBeatmapDifficultyCache.cs | 2 +- .../Database/BeatmapImporterTests.cs | 24 ++--- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 6 +- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 2 +- .../Visual/Beatmaps/TestSceneBeatmapCard.cs | 2 +- .../Editing/TestSceneDifficultySwitching.cs | 2 +- .../Editing/TestSceneEditorBeatmapCreation.cs | 4 +- .../Editing/TestSceneEditorTestGameplay.cs | 2 +- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 2 +- .../TestSceneDrawableRoomPlaylist.cs | 6 +- .../TestSceneMultiSpectatorScreen.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 4 +- .../TestSceneMultiplayerMatchSubScreen.cs | 2 +- .../TestSceneMultiplayerPlaylist.cs | 2 +- .../TestSceneMultiplayerQueueList.cs | 4 +- .../TestSceneMultiplayerReadyButton.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 2 +- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- .../TestSceneMouseWheelVolumeAdjust.cs | 2 +- .../Navigation/TestScenePerformFromScreen.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 6 +- .../Online/TestSceneBeatmapDownloadButton.cs | 2 +- .../TestScenePlaylistsRoomCreation.cs | 18 +++- ...tSceneUpdateableBeatmapBackgroundSprite.cs | 2 +- 26 files changed, 146 insertions(+), 47 deletions(-) create mode 100644 osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs diff --git a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs new file mode 100644 index 0000000000..8e67ecd818 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs @@ -0,0 +1,87 @@ +// 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.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Tests.Database; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.IO +{ + [TestFixture] + public static class BeatmapImportHelper + { + public static async Task LoadQuickOszIntoOsu(OsuGameBase osu) + { + string temp = TestResources.GetQuickTestBeatmapForImport(); + + var manager = osu.Dependencies.Get(); + + var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false); + + Debug.Assert(importedSet != null); + + ensureLoaded(osu); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + } + + public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) + { + string temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); + + var manager = osu.Dependencies.Get(); + + var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false); + + Debug.Assert(importedSet != null); + + ensureLoaded(osu); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + } + + private static void ensureLoaded(OsuGameBase osu, int timeout = 60000) + { + var realmContextFactory = osu.Dependencies.Get(); + + using (var realm = realmContextFactory.CreateContext()) + BeatmapImporterTests.EnsureLoaded(realm, timeout); + + // TODO: add back some extra checks outside of the realm ones? + // var set = queryBeatmapSets().First(); + // foreach (BeatmapInfo b in set.Beatmaps) + // Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID)); + // Assert.IsTrue(set.Beatmaps.Count > 0); + // var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap; + // Assert.IsTrue(beatmap?.HitObjects.Any() == true); + // beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap; + // Assert.IsTrue(beatmap?.HitObjects.Any() == true); + // beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap; + // Assert.IsTrue(beatmap?.HitObjects.Any() == true); + // beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap; + // Assert.IsTrue(beatmap?.HitObjects.Any() == true); + } + + private static void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) + { + Task task = Task.Run(() => + { + while (!result()) Thread.Sleep(200); + }); + + Assert.IsTrue(task.Wait(timeout), failureMessage); + } + } +} diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs index 298e474fd1..f3456cf8e4 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Beatmaps [BackgroundDependencyLoader] private void load(OsuGameBase osu) { - importedSet = ImportBeatmapTest.LoadQuickOszIntoOsu(osu).GetResultSafely(); + importedSet = BeatmapImportHelper.LoadQuickOszIntoOsu(osu).GetResultSafely(); } [SetUpSteps] diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index e144f863b0..5b63db1972 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Database using (var stream = File.OpenRead(tempPath)) { importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath))); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); } Assert.NotNull(importedSet); @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -242,7 +242,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); // check the newly "imported" beatmap is not the original. Assert.NotNull(importedSecondTime); @@ -291,7 +291,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -339,7 +339,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -606,7 +606,7 @@ namespace osu.Game.Tests.Database string? temp = TestResources.GetTestBeatmapForImport(); using (File.OpenRead(temp)) await importer.Import(temp); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); File.Delete(temp); Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); }); @@ -639,7 +639,7 @@ namespace osu.Game.Tests.Database await importer.Import(temp); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); } finally { @@ -679,7 +679,7 @@ namespace osu.Game.Tests.Database Assert.NotNull(imported); Debug.Assert(imported != null); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder"); } @@ -729,7 +729,7 @@ namespace osu.Game.Tests.Database Assert.NotNull(imported); Debug.Assert(imported != null); - ensureLoaded(realmFactory.Context); + EnsureLoaded(realmFactory.Context); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored"); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder"); @@ -772,7 +772,7 @@ namespace osu.Game.Tests.Database Assert.NotNull(importedSet); - ensureLoaded(realm); + EnsureLoaded(realm); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); @@ -788,7 +788,7 @@ namespace osu.Game.Tests.Database Assert.NotNull(importedSet); Debug.Assert(importedSet != null); - ensureLoaded(realm); + EnsureLoaded(realm); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); @@ -849,7 +849,7 @@ namespace osu.Game.Tests.Database Assert.AreEqual(expected, singleReferencedCount); } - private static void ensureLoaded(Realm realm, int timeout = 60000) + internal static void EnsureLoaded(Realm realm, int timeout = 60000) { IQueryable? resultSets = null; diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 5a2349e776..bdc2317ccd 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Online var existing = beatmaps.QueryBeatmapSet(s => s.OnlineID == testBeatmapSet.OnlineID); if (existing != null) - beatmaps.Delete(existing); + beatmaps.Delete(existing.Value); selectedItem.Value = new PlaylistItem { @@ -103,10 +103,10 @@ namespace osu.Game.Tests.Online AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely()); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); - AddStep("delete beatmap", () => beatmaps.Delete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID))); + AddStep("delete beatmap", () => beatmaps.Delete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID)!.Value)); addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); - AddStep("undelete beatmap", () => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID))); + AddStep("undelete beatmap", () => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID)!.Value)); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); } diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index f00fb97dfa..3e6a877a35 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Scores.IO var beatmapManager = osu.Dependencies.Get(); var scoreManager = osu.Dependencies.Get(); - beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))); + beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))!.Value); Assert.That(scoreManager.Query(s => s.Equals(imported)).Value.DeletePending, Is.EqualTo(true)); var secondImport = await LoadScoreIntoOsu(osu, imported); diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index 7b5e1f4ec7..94b693363a 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -204,7 +204,7 @@ namespace osu.Game.Tests.Visual.Beatmaps { var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == online_id); - if (beatmap != null) beatmaps.Delete(beatmap); + if (beatmap != null) beatmaps.Delete(beatmap.Value); }); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index 516305079b..243bb71e26 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Editing public override void SetUpSteps() { - AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely()); + AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely()); base.SetUpSteps(); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index db20d3c7ba..4cc6e2ca18 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.IsManaged); - AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false); + AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == false); } [Test] @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Editing }); AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen()); - AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true); + AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true); } [Test] diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index 6d48ef3ba7..bb630e5d5c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Editing public override void SetUpSteps() { - AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game).GetResultSafely()); + AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely()); base.SetUpSteps(); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index d8964c531f..8b7e1a1d85 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("import beatmap", () => { - importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely(); + importedBeatmap = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely(); importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineID; }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index a993190a67..dc826701d2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); InitialBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 1607750d15..d87a0a4a1c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -154,16 +154,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo; + Debug.Assert(beatmap.BeatmapSet != null); + AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).WaitSafely()); createPlaylistWithBeatmaps(beatmap); assertDownloadButtonVisible(false); - AddStep("delete beatmap set", () => manager.Delete(manager.QueryBeatmapSets(_ => true).Single())); + AddStep("delete beatmap set", () => manager.Delete(manager.QueryBeatmapSets(_ => true).Single().Value)); assertDownloadButtonVisible(true); - AddStep("undelete beatmap set", () => manager.Undelete(manager.QueryBeatmapSets(_ => true).Single())); + AddStep("undelete beatmap set", () => manager.Undelete(manager.QueryBeatmapSets(_ => true).Single().Value)); assertDownloadButtonVisible(false); void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}", diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index c78d1a2f62..9d67742e4d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load() { - importedSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely(); + importedSet = BeatmapImportHelper.LoadOszIntoOsu(game, virtualTrack: true).GetResultSafely(); importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); importedBeatmapId = importedBeatmap.OnlineID; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 18f45086f6..471816edf8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); }); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); @@ -588,7 +588,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("restore beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); }); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is SpectatorScreen); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 6b6a02906e..98b14fbddb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); } [SetUp] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 251f407a85..813b62176b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index ad47745642..b5d31c7ae9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void load(GameHost host, AudioManager audio) { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); } public override void SetUpSteps() @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 59e5318263..0b2ab1aef3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AvailabilityTracker.SelectedItem.BindTo(selectedItem); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); selectedItem.Value = new PlaylistItem { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 5466a19657..6b1001f6b7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AvailabilityTracker.SelectedItem.BindTo(selectedItem); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); selectedItem.Value = new PlaylistItem { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 2140acb2e3..97d17ada71 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable().First(); + importedSet = beatmaps.GetAllUsableBeatmapSets().First(); }); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs index 701ab480f6..22a00a3e5a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneMouseWheelVolumeAdjust.cs @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); - AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); AddStep("press enter", () => InputManager.Key(Key.Enter)); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index 24f5808961..1ebceed15d 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Navigation private void importAndWaitForSongSelect() { - AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); PushAndConfirm(() => new TestPlaySongSelect()); AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID == 241526); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d094d8b688..0349961f75 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); - AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); - AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); - AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game, virtualTrack: true).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapDownloadButton.cs index 21bf8d1c5a..d9f01622da 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapDownloadButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapDownloadButton.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online { var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == 241526); - if (beatmap != null) beatmaps.Delete(beatmap); + if (beatmap != null) beatmaps.Delete(beatmap.Value); }); } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index e59884f4f4..908e93dee2 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -39,7 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists private void load(GameHost host, AudioManager audio) { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); } [SetUpSteps] @@ -122,8 +123,8 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("store real beatmap values", () => { realHash = importedBeatmap.Value.Beatmaps[0].MD5Hash; - realOnlineId = importedBeatmap.Value.Beatmaps[0].OnlineID ?? -1; - realOnlineSetId = importedBeatmap.Value.OnlineID ?? -1; + realOnlineId = importedBeatmap.Value.Beatmaps[0].OnlineID; + realOnlineSetId = importedBeatmap.Value.OnlineID; }); AddStep("import modified beatmap", () => @@ -185,6 +186,8 @@ namespace osu.Game.Tests.Visual.Playlists }, }; + Debug.Assert(originalBeatmap.BeatmapInfo.BeatmapSet != null); + manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet).WaitSafely(); }); @@ -202,7 +205,14 @@ namespace osu.Game.Tests.Visual.Playlists }); } - private void importBeatmap() => AddStep("import beatmap", () => importedBeatmap = manager.Import(CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).GetResultSafely()); + private void importBeatmap() => AddStep("import beatmap", () => + { + var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo); + + Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); + + importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet).GetResultSafely(); + }); private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs index 6fe1ccc037..4f22281818 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface { this.api = api; - testBeatmap = ImportBeatmapTest.LoadOszIntoOsu(osu).GetResultSafely(); + testBeatmap = BeatmapImportHelper.LoadOszIntoOsu(osu).GetResultSafely(); } [Test] From 1f9318265e6a7ee5a89b356d8a0e1418463ca066 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 18:45:22 +0900 Subject: [PATCH 049/227] Update `ToLive` usages in line with recent changes --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 6 +++--- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Scoring/ScoreManager.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 471816edf8..d55ca0b578 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void load(GameHost host, AudioManager audio) { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); } public override void SetUpSteps() @@ -827,7 +827,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { - BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1 + BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID }))); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); @@ -858,7 +858,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 })); AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem { - BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1 + BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID }))); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 706160bfb5..462980448a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -166,7 +166,7 @@ namespace osu.Game.Beatmaps return context.All() .Where(b => !b.DeletePending) .Where(query) - .ToLive(); + .ToLive(contextFactory); } } @@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps public ILive? QueryBeatmapSet(Expression> query) { using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(); + return context.All().FirstOrDefault(query)?.ToLive(contextFactory); } /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 0a1b616422..c2a9675474 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -52,7 +52,7 @@ namespace osu.Game.Scoring public ILive Query(Expression> query) { using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(); + return context.All().FirstOrDefault(query)?.ToLive(contextFactory); } /// diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index bb8ddb8cd6..f54228b441 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -82,9 +82,9 @@ namespace osu.Game.Screens.Menu } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, SkinManager skinManager, BeatmapManager beatmaps, Framework.Game game) + private void load(OsuConfigManager config, SkinManager skinManager, BeatmapManager beatmaps, Framework.Game game, RealmContextFactory realmContextFactory) { - // prevent user from changing beatmap while the intro is still runnning. + // prevent user from changing beatmap while the intro is still running. beatmap = Beatmap.BeginLease(false); MenuVoice = config.GetBindable(OsuSetting.MenuVoice); @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Menu if (sets.Count > 0) { setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo?.PerformRead(s => s.Beatmaps[0].ToLive())); + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo?.PerformRead(s => s.Beatmaps[0].ToLive(realmContextFactory))); } } @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Menu if (setInfo == null) return false; - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.PerformRead(s => s.Beatmaps[0].ToLive())); + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.PerformRead(s => s.Beatmaps[0].ToLive(realmContextFactory))); return UsingThemedIntro = initialBeatmap != null; } From 5dc497e949cf813f57bc35f4ad8bfc8d488fb6a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 18:37:28 +0900 Subject: [PATCH 050/227] Replace `BeatmapLeaderboard` event flow with realm subscriptions --- .../Select/Leaderboards/BeatmapLeaderboard.cs | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index e47acc5ba7..869036ef68 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -44,6 +44,8 @@ namespace osu.Game.Screens.Select.Leaderboards Scores = null; UpdateScores(); + if (IsLoaded) + refreshRealmSubscription(); } } @@ -78,6 +80,9 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private RealmContextFactory realmContextFactory { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -87,9 +92,32 @@ namespace osu.Game.Screens.Select.Leaderboards if (filterMods) UpdateScores(); }; + } - scoreManager.ItemRemoved += scoreStoreChanged; - scoreManager.ItemUpdated += scoreStoreChanged; + protected override void LoadComplete() + { + base.LoadComplete(); + + refreshRealmSubscription(); + } + + private IDisposable scoreSubscription; + + private void refreshRealmSubscription() + { + scoreSubscription?.Dispose(); + scoreSubscription = null; + + if (beatmapInfo == null) + return; + + scoreSubscription = realmContextFactory.Context.All().Where(s => s.BeatmapInfo.ID == beatmapInfo.ID).QueryAsyncWithNotifications((_, changes, ___) => + { + if (changes == null) + return; + + RefreshScores(); + }); } protected override void Reset() @@ -212,11 +240,7 @@ namespace osu.Game.Screens.Select.Leaderboards { base.Dispose(isDisposing); - if (scoreManager != null) - { - scoreManager.ItemRemoved -= scoreStoreChanged; - scoreManager.ItemUpdated -= scoreStoreChanged; - } + scoreSubscription?.Dispose(); } } } From 00e9f0d41ee90143a107b397a6fcd9c328ed2383 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 18:53:46 +0900 Subject: [PATCH 051/227] Replace `BeatmapDownloadTracker` event flow with realm subscriptions --- osu.Game/Online/BeatmapDownloadTracker.cs | 62 ++++++++++----------- osu.Game/Stores/RealmArchiveModelManager.cs | 1 + 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 509d5c1b71..7a396ac7aa 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API; #nullable enable @@ -12,37 +14,47 @@ namespace osu.Game.Online { public class BeatmapDownloadTracker : DownloadTracker { - [Resolved(CanBeNull = true)] - protected BeatmapManager? Manager { get; private set; } - [Resolved(CanBeNull = true)] protected BeatmapModelDownloader? Downloader { get; private set; } private ArchiveDownloadRequest? attachedRequest; + private IDisposable? realmSubscription; + + [Resolved] + private RealmContextFactory realmContextFactory { get; set; } = null!; + public BeatmapDownloadTracker(IBeatmapSetInfo trackedItem) : base(trackedItem) { } - [BackgroundDependencyLoader(true)] - private void load() + protected override void LoadComplete() { - if (Manager == null || Downloader == null) + base.LoadComplete(); + + if (Downloader == null) return; + Downloader.DownloadBegan += downloadBegan; + Downloader.DownloadFailed += downloadFailed; + // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - if (Manager.IsAvailableLocally(beatmapSetInfo)) - UpdateState(DownloadState.LocallyAvailable); - else - attachDownload(Downloader.GetExistingDownload(beatmapSetInfo)); - - Downloader.DownloadBegan += downloadBegan; - Downloader.DownloadFailed += downloadFailed; - Manager.ItemUpdated += itemUpdated; - Manager.ItemRemoved += itemRemoved; + realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID).QueryAsyncWithNotifications((items, changes, ___) => + { + if (items.Any()) + Schedule(() => UpdateState(DownloadState.LocallyAvailable)); + else + { + Schedule(() => + { + UpdateState(DownloadState.NotDownloaded); + attachDownload(Downloader.GetExistingDownload(beatmapSetInfo)); + }); + } + }); } private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => @@ -97,18 +109,6 @@ namespace osu.Game.Online private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null)); - private void itemUpdated(BeatmapSetInfo item) => Schedule(() => - { - if (checkEquality(item, TrackedItem)) - UpdateState(DownloadState.LocallyAvailable); - }); - - private void itemRemoved(BeatmapSetInfo item) => Schedule(() => - { - if (checkEquality(item, TrackedItem)) - UpdateState(DownloadState.NotDownloaded); - }); - private bool checkEquality(IBeatmapSetInfo x, IBeatmapSetInfo y) => x.OnlineID == y.OnlineID; #region Disposal @@ -118,17 +118,13 @@ namespace osu.Game.Online base.Dispose(isDisposing); attachDownload(null); + realmSubscription?.Dispose(); + if (Downloader != null) { Downloader.DownloadBegan -= downloadBegan; Downloader.DownloadFailed -= downloadFailed; } - - if (Manager != null) - { - Manager.ItemUpdated -= itemUpdated; - Manager.ItemRemoved -= itemRemoved; - } } #endregion diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 7fa8d16882..d6bb01cedf 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -187,6 +187,7 @@ namespace osu.Game.Stores item.Realm.Write(r => item.DeletePending = false); } + // TODO: delete or abstract public virtual bool IsAvailableLocally(TModel model) => false; // Not relevant for skins since they can't be downloaded yet. public void Update(TModel skin) From 8c4836e87d68cfeef0b2b51527e7364039b6f01a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 18:58:21 +0900 Subject: [PATCH 052/227] Replace `ScoreDownloadTracker` event flow with realm subscriptions --- osu.Game/Online/ScoreDownloadTracker.cs | 58 ++++++++++++------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 68932cc388..946a751c31 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; +using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Scoring; @@ -13,23 +15,26 @@ namespace osu.Game.Online { public class ScoreDownloadTracker : DownloadTracker { - [Resolved(CanBeNull = true)] - protected ScoreManager? Manager { get; private set; } - [Resolved(CanBeNull = true)] protected ScoreModelDownloader? Downloader { get; private set; } private ArchiveDownloadRequest? attachedRequest; + private IDisposable? realmSubscription; + + [Resolved] + private RealmContextFactory realmContextFactory { get; set; } = null!; + public ScoreDownloadTracker(ScoreInfo trackedItem) : base(trackedItem) { } - [BackgroundDependencyLoader(true)] - private void load() + protected override void LoadComplete() { - if (Manager == null || Downloader == null) + base.LoadComplete(); + + if (Downloader == null) return; // Used to interact with manager classes that don't support interface types. Will eventually be replaced. @@ -39,15 +44,22 @@ namespace osu.Game.Online OnlineID = TrackedItem.OnlineID }; - if (Manager.IsAvailableLocally(scoreInfo)) - UpdateState(DownloadState.LocallyAvailable); - else - attachDownload(Downloader.GetExistingDownload(scoreInfo)); - Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - Manager.ItemUpdated += itemUpdated; - Manager.ItemRemoved += itemRemoved; + + realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID).QueryAsyncWithNotifications((items, changes, ___) => + { + if (items.Any()) + Schedule(() => UpdateState(DownloadState.LocallyAvailable)); + else + { + Schedule(() => + { + UpdateState(DownloadState.NotDownloaded); + attachDownload(Downloader.GetExistingDownload(scoreInfo)); + }); + } + }); } private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => @@ -102,18 +114,6 @@ namespace osu.Game.Online private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null)); - private void itemUpdated(ScoreInfo item) => Schedule(() => - { - if (checkEquality(item, TrackedItem)) - UpdateState(DownloadState.LocallyAvailable); - }); - - private void itemRemoved(ScoreInfo item) => Schedule(() => - { - if (checkEquality(item, TrackedItem)) - UpdateState(DownloadState.NotDownloaded); - }); - private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.MatchesOnlineID(y); #region Disposal @@ -123,17 +123,13 @@ namespace osu.Game.Online base.Dispose(isDisposing); attachDownload(null); + realmSubscription?.Dispose(); + if (Downloader != null) { Downloader.DownloadBegan -= downloadBegan; Downloader.DownloadFailed -= downloadFailed; } - - if (Manager != null) - { - Manager.ItemUpdated -= itemUpdated; - Manager.ItemRemoved -= itemRemoved; - } } #endregion From c9257e9ecc34e484bebe1a70ec663268f729a915 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 19:01:19 +0900 Subject: [PATCH 053/227] Fix missing disposal of realm subscriptions in `BeatmapCarousel` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 23 +++++++++------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 94e7dc9241..78e416c64b 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -144,9 +144,13 @@ namespace osu.Game.Screens.Select private CarouselRoot root; + private IDisposable subscriptionSets; + private IDisposable subscriptionBeatmaps; + private readonly DrawablePool setPool = new DrawablePool(100); public BeatmapCarousel() + { root = new CarouselRoot(this); InternalChild = new OsuContextMenuContainer @@ -163,10 +167,7 @@ namespace osu.Game.Screens.Select }; } - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [BackgroundDependencyLoader(permitNulls: true)] + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); @@ -183,13 +184,10 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - realmFactory.Context.All().Where(s => !s.DeletePending).QueryAsyncWithNotifications(beatmapSetsChanged); - realmFactory.Context.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); + subscriptionSets = realmFactory.Context.All().Where(s => !s.DeletePending).QueryAsyncWithNotifications(beatmapSetsChanged); + subscriptionBeatmaps = realmFactory.Context.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); } - private void beatmapRemoved(BeatmapSetInfo item) => RemoveBeatmapSet(item); - private void beatmapUpdated(BeatmapSetInfo item) => UpdateBeatmapSet(item); - private void beatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) { if (changes == null) @@ -921,11 +919,8 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); - if (beatmaps != null) - { - beatmaps.ItemUpdated -= beatmapUpdated; - beatmaps.ItemRemoved -= beatmapRemoved; - } + subscriptionSets?.Dispose(); + subscriptionBeatmaps?.Dispose(); } } } From fe8a5e867d2cbaa19e95516c61084508ff57af2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 19:03:02 +0900 Subject: [PATCH 054/227] Remove updated/removed flow method mapping --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ---- osu.Game/Database/IModelManager.cs | 11 ----------- osu.Game/Scoring/ScoreManager.cs | 12 ------------ osu.Game/Stores/RealmArchiveModelManager.cs | 16 ---------------- 4 files changed, 43 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 462980448a..d9484a2880 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -232,10 +232,6 @@ namespace osu.Game.Beatmaps return beatmapModelManager.IsAvailableLocally(model); } - public event Action? ItemUpdated; - - public event Action? ItemRemoved; - public void Update(BeatmapSetInfo item) { beatmapModelManager.Update(item); diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 779d0522f7..e7218b621c 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.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; using System.Collections.Generic; namespace osu.Game.Database @@ -13,16 +12,6 @@ namespace osu.Game.Database public interface IModelManager where TModel : class { - /// - /// Fired when an item is updated. - /// - event Action ItemUpdated; - - /// - /// Fired when an item is removed. - /// - event Action ItemRemoved; - /// /// Perform an update of the specified item. /// TODO: Support file additions/removals. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index c2a9675474..159c3c2da0 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -251,18 +251,6 @@ namespace osu.Game.Scoring #region Implementation of IModelManager - public event Action ItemUpdated - { - add => scoreModelManager.ItemUpdated += value; - remove => scoreModelManager.ItemUpdated -= value; - } - - public event Action ItemRemoved - { - add => scoreModelManager.ItemRemoved += value; - remove => scoreModelManager.ItemRemoved -= value; - } - public void Update(ScoreInfo item) { scoreModelManager.Update(item); diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index d6bb01cedf..8698799b4a 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -24,22 +24,6 @@ namespace osu.Game.Stores public abstract class RealmArchiveModelManager : RealmArchiveModelImporter, IModelManager, IModelFileManager where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete { - public event Action? ItemUpdated - { - // This may be brought back for beatmaps to ease integration. - // The eventual goal would be not requiring this and using realm subscriptions in its place. - add => throw new NotImplementedException(); - remove => throw new NotImplementedException(); - } - - public event Action? ItemRemoved - { - // This may be brought back for beatmaps to ease integration. - // The eventual goal would be not requiring this and using realm subscriptions in its place. - add => throw new NotImplementedException(); - remove => throw new NotImplementedException(); - } - private readonly RealmFileStore realmFileStore; protected RealmArchiveModelManager(Storage storage, RealmContextFactory contextFactory) From 6d60aa7d9cc45799c40c72f119db2c0936aa606f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 19:09:35 +0900 Subject: [PATCH 055/227] Replace `TopLocalRank` event flow with realm subscriptions --- .../Screens/Select/Carousel/TopLocalRank.cs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index a83cb08e1f..2ade213e8b 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -1,6 +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; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,9 +20,6 @@ namespace osu.Game.Screens.Select.Carousel { private readonly BeatmapInfo beatmapInfo; - [Resolved] - private ScoreManager scores { get; set; } - [Resolved] private IBindable ruleset { get; set; } @@ -40,26 +38,34 @@ namespace osu.Game.Screens.Select.Carousel [BackgroundDependencyLoader] private void load() { - scores.ItemUpdated += scoreChanged; - scores.ItemRemoved += scoreChanged; - ruleset.ValueChanged += _ => fetchAndLoadTopScore(); fetchAndLoadTopScore(); } - private void scoreChanged(ScoreInfo score) + protected override void LoadComplete() { - if (score.BeatmapInfoID == beatmapInfo.ID) - fetchAndLoadTopScore(); + base.LoadComplete(); + + scoreSubscription = realmFactory.Context.All().Where(s => s.BeatmapInfo.ID == beatmapInfo.ID).QueryAsyncWithNotifications((_, changes, ___) => + { + if (changes == null) + return; + + fetchTopScoreRank(); + }); } + private IDisposable scoreSubscription; + private ScheduledDelegate scheduledRankUpdate; private void fetchAndLoadTopScore() { + // TODO: this lookup likely isn't required, we can use the results of the subscription directly. var rank = fetchTopScoreRank(); - scheduledRankUpdate = Schedule(() => + + scheduledRankUpdate = Scheduler.Add(() => { Rank = rank; @@ -89,11 +95,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Dispose(isDisposing); - if (scores != null) - { - scores.ItemUpdated -= scoreChanged; - scores.ItemRemoved -= scoreChanged; - } + scoreSubscription?.Dispose(); } } } From 5c0d31ed2444ed5785584691a07e38caab2a931a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 19:21:41 +0900 Subject: [PATCH 056/227] Replace `OnlinePlayBeatmapAvailabilityTracker` event flow with realm subscriptions --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index a32f069470..01999ba766 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Database; namespace osu.Game.Online.Rooms { @@ -29,6 +31,9 @@ namespace osu.Game.Online.Rooms [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved] + private RealmContextFactory realmContextFactory { get; set; } = null!; + /// /// The availability state of the currently selected playlist item. /// @@ -45,6 +50,8 @@ namespace osu.Game.Online.Rooms /// private BeatmapInfo matchingHash; + private IDisposable realmSubscription; + protected override void LoadComplete() { base.LoadComplete(); @@ -75,27 +82,24 @@ namespace osu.Game.Online.Rooms if (progressUpdate?.Completed != false) progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); }, true); + + // These events are needed for a fringe case where a modified/altered beatmap is imported with matching OnlineIDs. + // During the import process this will cause the existing beatmap set to be silently deleted and replaced with the new one. + // This is not exposed to us via `BeatmapDownloadTracker` so we have to take it into our own hands (as we care about the hash matching). + realmSubscription?.Dispose(); + realmSubscription = realmContextFactory.Context + .All() + .Where(s => s.OnlineID == SelectedItem.Value.BeatmapID || (matchingHash != null && s.ID == matchingHash.ID)) + .QueryAsyncWithNotifications((items, changes, ___) => + { + if (changes == null) + return; + + Schedule(updateAvailability); + }); }, true); - - // These events are needed for a fringe case where a modified/altered beatmap is imported with matching OnlineIDs. - // During the import process this will cause the existing beatmap set to be silently deleted and replaced with the new one. - // This is not exposed to us via `BeatmapDownloadTracker` so we have to take it into our own hands (as we care about the hash matching). - beatmapManager.ItemUpdated += itemUpdated; - beatmapManager.ItemRemoved += itemRemoved; } - private void itemUpdated(BeatmapSetInfo item) => Schedule(() => - { - if (matchingHash?.BeatmapSet.ID == item.ID || SelectedItem.Value?.Beatmap.Value.BeatmapSet?.OnlineID == item.OnlineID) - updateAvailability(); - }); - - private void itemRemoved(BeatmapSetInfo item) => Schedule(() => - { - if (matchingHash?.BeatmapSet.ID == item.ID) - updateAvailability(); - }); - private void updateAvailability() { if (downloadTracker == null) @@ -148,11 +152,7 @@ namespace osu.Game.Online.Rooms { base.Dispose(isDisposing); - if (beatmapManager != null) - { - beatmapManager.ItemUpdated -= itemUpdated; - beatmapManager.ItemRemoved -= itemRemoved; - } + realmSubscription?.Dispose(); } } } From 86ce2256be0a1619c81fca24618c145bb227933f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 19:26:41 +0900 Subject: [PATCH 057/227] Replace `SpectatorScreen` event flow with realm subscriptions --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index c4e75cc413..dd586bdd37 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -1,6 +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; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -54,6 +55,11 @@ namespace osu.Game.Screens.Spectate this.users.AddRange(users); } + [Resolved] + private RealmContextFactory realmContextFactory { get; set; } + + private IDisposable realmSubscription; + protected override void LoadComplete() { base.LoadComplete(); @@ -73,7 +79,17 @@ namespace osu.Game.Screens.Spectate playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); - beatmaps.ItemUpdated += beatmapUpdated; + realmSubscription = realmContextFactory.Context + .All() + .Where(s => !s.DeletePending) + .QueryAsyncWithNotifications((items, changes, ___) => + { + if (changes?.InsertedIndices == null) + return; + + foreach (int c in changes.InsertedIndices) + beatmapUpdated(items[c]); + }); foreach ((int id, var _) in userMap) spectatorClient.WatchUser(id); @@ -219,8 +235,7 @@ namespace osu.Game.Screens.Spectate spectatorClient.StopWatchingUser(userId); } - if (beatmaps != null) - beatmaps.ItemUpdated -= beatmapUpdated; + realmSubscription?.Dispose(); } } } From 4295815c7d44800959c90b6284f85c419128058f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Dec 2021 19:39:04 +0900 Subject: [PATCH 058/227] Fix invalid equality comparison in `BeatmapLeaderboard` --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 869036ef68..0bc93fc36e 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Select.Leaderboards get => beatmapInfo; set { - if (beatmapInfo.Equals(value)) + if (beatmapInfo?.Equals(value) == true) return; beatmapInfo = value; From 99e46cd26b30007f1f919840711814a58bbbc51d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 16:09:00 +0900 Subject: [PATCH 059/227] Fix missing `BeatmapMetadata.ToString` This is relied on by a few usages. --- osu.Game/Beatmaps/BeatmapMetadata.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index dc5517375d..f729b55154 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -54,5 +54,7 @@ namespace osu.Game.Beatmaps } #endregion + + public override string ToString() => this.GetDisplayTitle(); } } From 167c399e8ada3ce3d47870d14b4e70235099ed81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 16:09:19 +0900 Subject: [PATCH 060/227] Fix invalid DI resolution of `RealmFileStore` --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index d21813d97f..3d6240bc98 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; +using osu.Game.Database; using osu.Game.Screens.Play; using osu.Game.Stores; @@ -76,12 +77,12 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(RealmFileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken, GameHost host) + private void load(GameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmContextFactory realmContextFactory) { if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); + dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(new RealmFileStore(realmContextFactory, host.Storage).Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { From 2a980cc474a70993b160da4eb6c4ce80f0c8d634 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 16:09:32 +0900 Subject: [PATCH 061/227] Fix `BeatmapInfo` file lookup not handling the case where no files exist Quite common for test scenes. --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index fc0d007df4..16133ae362 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -38,7 +38,7 @@ namespace osu.Game.Beatmaps public BeatmapSetInfo? BeatmapSet { get; set; } [Ignored] - public RealmNamedFileUsage? File => BeatmapSet?.Files.First(f => f.File.Hash == Hash); + public RealmNamedFileUsage? File => BeatmapSet?.Files.FirstOrDefault(f => f.File.Hash == Hash); public BeatmapOnlineStatus Status { From dcd69e852eb0a516fc4d7bdfe27b23503f5d66b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 16:30:34 +0900 Subject: [PATCH 062/227] Add back settable `RulesetID` for now --- osu.Game/Beatmaps/BeatmapInfo.cs | 15 ++++++++++++++- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 4 +--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 16133ae362..ea2508032e 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -140,8 +140,21 @@ namespace osu.Game.Beatmaps #region Compatibility properties + private int rulesetID; + [Ignored] - public int RulesetID => Ruleset.OnlineID; + public int RulesetID + { + // ReSharper disable once ConstantConditionalAccessQualifier + get => Ruleset?.OnlineID ?? rulesetID; + set + { + if (Ruleset != null) + throw new InvalidOperationException($"Cannot set a {nameof(RulesetID)} when {nameof(Ruleset)} is non-null"); + + rulesetID = value; + } + } [Ignored] public Guid BeatmapSetInfoID => BeatmapSet?.ID ?? Guid.Empty; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 5c61d302c2..e5db9d045a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.IO; -using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Beatmaps.Formats @@ -142,8 +141,7 @@ namespace osu.Game.Beatmaps.Formats break; case @"Mode": - // TODO: ha ha ha. - beatmap.BeatmapInfo.Ruleset = new RulesetInfo(Parsing.ParseInt(pair.Value), "some ruleset", "wangs", true /* probably not */); + beatmap.BeatmapInfo.RulesetID = Parsing.ParseInt(pair.Value); switch (beatmap.BeatmapInfo.RulesetID) { From 0793b0f0abf827976f0b813170ff933d44ea31fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Dec 2021 16:37:29 +0900 Subject: [PATCH 063/227] Fix `Max` lookup methods not checking for zero beatmap count --- osu.Game/Beatmaps/BeatmapSetInfo.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 4aea0c6ce8..ed16e3a965 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -50,11 +50,11 @@ namespace osu.Game.Beatmaps /// public bool Protected { get; set; } - public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating); + public double MaxStarDifficulty => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.StarRating); - public double MaxLength => Beatmaps.Max(b => b.Length); + public double MaxLength => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.Length); - public double MaxBPM => Beatmaps.Max(b => b.BPM); + public double MaxBPM => Beatmaps.Count == 0 ? 0 : Beatmaps.Max(b => b.BPM); /// /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. From 33060990b7741ec2823a58fb590c4a4da554a8c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Dec 2021 17:05:00 +0900 Subject: [PATCH 064/227] Temporarily disable `WorkingBeatmapCache` and fix multiple invalid data flows --- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 19 ++-------- osu.Game/Overlays/MusicController.cs | 36 ++++++++++--------- osu.Game/Screens/Menu/IntroScreen.cs | 32 +++++++++++++++-- .../Select/Leaderboards/BeatmapLeaderboard.cs | 11 ------ .../Visual/RateAdjustedBeatmapTestScene.cs | 7 ++-- 5 files changed, 57 insertions(+), 48 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 95983ae052..888851d1e8 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -12,7 +12,6 @@ using osu.Framework.IO.Stores; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.Database; @@ -86,23 +85,11 @@ namespace osu.Game.Beatmaps if (beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; - lock (workingCache) - { - var working = workingCache.FirstOrDefault(w => beatmapInfo.Equals(w.BeatmapInfo)); + WorkingBeatmap working = null; - if (working != null) - return working; + working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this); - // TODO: is this still required..? - //beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); - - // best effort; may be higher than expected. - GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); - - return working; - } + return working; } #region IResourceStorageProvider diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 8c115c2c94..2731096a00 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -71,33 +71,37 @@ namespace osu.Game.Overlays [Resolved] private RealmContextFactory realmFactory { get; set; } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - - beatmapSubscription = realmFactory.Context.All().QueryAsyncWithNotifications(beatmapsChanged); - // Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now. // They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load(). beatmap.BindValueChanged(beatmapChanged, true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + // ensure we're ready before completing async load. + // probably not a good way of handling this (as there is a period we aren't watching for changes until the realm subscription finishes up. + foreach (var s in realmFactory.Context.All()) + beatmapSets.Add(s); + + beatmapSubscription = realmFactory.Context.All().QueryAsyncWithNotifications(beatmapsChanged); + } + private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) { if (changes == null) - { - beatmapSets.AddRange(sender); - } + return; - // beatmaps.ItemUpdated += set => Schedule(() => - // { - // beatmapSets.Remove(set); - // beatmapSets.Add(set); - // }); - // beatmaps.ItemRemoved += set => Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID)); - // - // beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next())); + foreach (int i in changes.InsertedIndices) + beatmapSets.Insert(i, sender[i]); + + foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) + beatmapSets.RemoveAt(i); } /// diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index f54228b441..e02066560d 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -81,8 +81,11 @@ namespace osu.Game.Screens.Menu this.createNextScreen = createNextScreen; } + [Resolved] + private BeatmapManager beatmaps { get; set; } + [BackgroundDependencyLoader] - private void load(OsuConfigManager config, SkinManager skinManager, BeatmapManager beatmaps, Framework.Game game, RealmContextFactory realmContextFactory) + private void load(OsuConfigManager config, SkinManager skinManager, Framework.Game game, RealmContextFactory realmContextFactory) { // prevent user from changing beatmap while the intro is still running. beatmap = Beatmap.BeginLease(false); @@ -101,7 +104,13 @@ namespace osu.Game.Screens.Menu if (sets.Count > 0) { setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo?.PerformRead(s => s.Beatmaps[0].ToLive(realmContextFactory))); + setInfo?.PerformRead(s => + { + if (s.Beatmaps.Count == 0) + return; + + initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0].ToLive(realmContextFactory)); + }); } } @@ -131,12 +140,29 @@ namespace osu.Game.Screens.Menu if (setInfo == null) return false; - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.PerformRead(s => s.Beatmaps[0].ToLive(realmContextFactory))); + setInfo.PerformRead(s => + { + if (s.Beatmaps.Count == 0) + return; + + initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0].ToLive(realmContextFactory)); + }); return UsingThemedIntro = initialBeatmap != null; } } + protected override void LoadComplete() + { + base.LoadComplete(); + + // TODO: This is temporary to get the setInfo on the update thread, to make things work "better" without using ILive everywhere. + var setInfo = beatmaps.QueryBeatmapSets(b => b.Hash == BeatmapHash).FirstOrDefault(); + + if (setInfo?.Value.Beatmaps.Count > 0) + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Value.Beatmaps.First()); + } + public override void OnResuming(IScreen last) { this.FadeIn(300); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 0bc93fc36e..c2ab930550 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -126,17 +126,6 @@ namespace osu.Game.Screens.Select.Leaderboards TopScore = null; } - private void scoreStoreChanged(ScoreInfo score) - { - if (Scope != BeatmapLeaderboardScope.Local) - return; - - if (BeatmapInfo?.ID != score.BeatmapInfoID) - return; - - RefreshScores(); - } - protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; private CancellationTokenSource loadCancellationSource; diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index ad24ffc7b8..5bac2635f5 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs @@ -12,8 +12,11 @@ namespace osu.Game.Tests.Visual { base.Update(); - // note that this will override any mod rate application - Beatmap.Value.Track.Tempo.Value = Clock.Rate; + if (Beatmap.Value.TrackLoaded && Beatmap.Value.Track != null) // null check... wasn't required until now? + { + // note that this will override any mod rate application + Beatmap.Value.Track.Tempo.Value = Clock.Rate; + } } } } From 667cdb2475279378beee6012ec59e7bc4018d636 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Dec 2021 17:09:24 +0900 Subject: [PATCH 065/227] Fix skin lookup when there's no beatmap file available --- osu.Game/Skinning/LegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e677e2c01b..81c695b8da 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -63,7 +63,7 @@ namespace osu.Game.Skinning /// Access to raw game resources. /// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file. protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string configurationFilename) - : this(skin, storage, resources, storage?.GetStream(configurationFilename)) + : this(skin, storage, resources, string.IsNullOrEmpty(configurationFilename) ? null : storage?.GetStream(configurationFilename)) { } From 3811bd85208e3667b3e6da0aa40c2616673a6588 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jan 2022 14:35:18 +0900 Subject: [PATCH 066/227] Fix some null inspections --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 4 ++-- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 6 +----- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs | 2 +- osu.Game/Screens/Edit/Verify/IssueList.cs | 2 +- osu.Game/Screens/Select/BeatmapDeleteDialog.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 8 ++++---- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 4 ++-- 9 files changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 586cd273be..7b81f330d0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -744,7 +744,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.OnlineID == 3); - AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmapInfo.BeatmapSet.OnlineID == previousSetID); + AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmapInfo.BeatmapSet?.OnlineID == previousSetID); AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID == 3); } @@ -760,7 +760,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets)).GetResultSafely().Value; + imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets)).GetResultSafely()?.Value; }); AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 888851d1e8..beec48becd 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -85,11 +85,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; - WorkingBeatmap working = null; - - working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this); - - return working; + return new BeatmapManagerWorkingBeatmap(beatmapInfo, this); } #region IResourceStorageProvider diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 2c78fa264e..5ef434c427 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -173,7 +173,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Text = score.MaxCombo.ToLocalisableString(@"0\x"), Font = OsuFont.GetFont(size: text_size), - Colour = score.MaxCombo == score.BeatmapInfo?.MaxCombo ? highAccuracyColour : Color4.White + Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White } }; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs index 255671c807..a285979fd2 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Edit.Checks foreach (string filename in videoPaths) { - string storagePath = beatmapSet.GetPathForFile(filename); + string storagePath = beatmapSet?.GetPathForFile(filename); if (storagePath == null) { diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index cadcdebc6e..5fe43199cc 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Verify private void load(OverlayColourProvider colours) { generalVerifier = new BeatmapVerifier(); - rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapVerifier(); + rulesetVerifier = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapVerifier(); context = new BeatmapVerifierContext(beatmap, workingBeatmap.Value, verify.InterpretedDifficulty.Value); verify.InterpretedDifficulty.BindValueChanged(difficulty => context.InterpretedDifficulty = difficulty.NewValue); diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs index 307c2352e3..1ac278d045 100644 --- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs +++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Select public BeatmapDeleteDialog(BeatmapSetInfo beatmap) { - BodyText = $@"{beatmap.Metadata?.Artist} - {beatmap.Metadata?.Title}"; + BodyText = $@"{beatmap.Metadata.Artist} - {beatmap.Metadata.Title}"; Icon = FontAwesome.Regular.TrashAlt; HeaderText = @"Confirm deletion of"; diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index a1a8de04ae..f2700e9053 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -63,16 +63,16 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Artist: - return string.Compare(BeatmapSet.Metadata?.Artist, otherSet.BeatmapSet.Metadata?.Artist, StringComparison.OrdinalIgnoreCase); + return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase); case SortMode.Title: - return string.Compare(BeatmapSet.Metadata?.Title, otherSet.BeatmapSet.Metadata?.Title, StringComparison.OrdinalIgnoreCase); + return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase); case SortMode.Author: - return string.Compare(BeatmapSet.Metadata?.Author.Username, otherSet.BeatmapSet.Metadata?.Author.Username, StringComparison.OrdinalIgnoreCase); + return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase); case SortMode.Source: - return string.Compare(BeatmapSet.Metadata?.Source, otherSet.BeatmapSet.Metadata?.Source, StringComparison.OrdinalIgnoreCase); + return string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase); case SortMode.DateAdded: return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 2eb226df70..a5fabd9237 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Select.Carousel }, new OsuSpriteText { - Text = $"{(beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata).Author.Username}", + Text = $"{beatmapInfo.Metadata.Author.Username}", Font = OsuFont.GetFont(italics: true), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 9aa85c872e..f2054677b0 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -41,13 +41,13 @@ namespace osu.Game.Screens.Select.Carousel { new OsuSpriteText { - Text = new RomanisableString(beatmapSet.Metadata?.TitleUnicode, beatmapSet.Metadata?.Title), + Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), Shadow = true, }, new OsuSpriteText { - Text = new RomanisableString(beatmapSet.Metadata?.ArtistUnicode, beatmapSet.Metadata?.Artist), + Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), Shadow = true, }, From de076678fe3120b32079f6356b87e4311e149256 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Jan 2022 15:59:41 +0900 Subject: [PATCH 067/227] Fix some remaining test failures --- osu.Game.Tests/Online/TestAPIModJsonSerialization.cs | 12 ++++++++++-- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 1 + osu.Game/Online/Solo/SubmittableScore.cs | 3 +-- osu.Game/Screens/Ranking/ScorePanelList.cs | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 4b160e1d67..1b7a7656b5 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -9,10 +9,12 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Solo; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -93,7 +95,11 @@ namespace osu.Game.Tests.Online [Test] public void TestDeserialiseSubmittableScoreWithEmptyMods() { - var score = new SubmittableScore(new ScoreInfo()); + var score = new SubmittableScore(new ScoreInfo + { + User = new APIUser(), + Ruleset = new OsuRuleset().RulesetInfo, + }); var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score)); @@ -105,7 +111,9 @@ namespace osu.Game.Tests.Online { var score = new SubmittableScore(new ScoreInfo { - Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } } + Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }, + User = new APIUser(), + Ruleset = new OsuRuleset().RulesetInfo, }); var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(score)); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 944941723e..ac736086fd 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -72,6 +72,7 @@ namespace osu.Game.Tests.Visual.Ranking Username = "peppy", }, BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, TotalScore = 2845370, Accuracy = accuracy, diff --git a/osu.Game/Online/Solo/SubmittableScore.cs b/osu.Game/Online/Solo/SubmittableScore.cs index 327806f390..4e4dae5157 100644 --- a/osu.Game/Online/Solo/SubmittableScore.cs +++ b/osu.Game/Online/Solo/SubmittableScore.cs @@ -10,7 +10,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Users; namespace osu.Game.Online.Solo { @@ -48,7 +47,7 @@ namespace osu.Game.Online.Solo public APIMod[] Mods { get; set; } [JsonProperty("user")] - public IUser User { get; set; } + public APIUser User { get; set; } [JsonProperty("statistics")] public Dictionary Statistics { get; set; } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 7f270b3a2a..c2ef5529e8 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Ranking trackingContainer.Show(); - if (SelectedScore.Value.Equals(score)) + if (SelectedScore.Value?.Equals(score) == true) { SelectedScore.TriggerChange(); } @@ -185,7 +185,7 @@ namespace osu.Game.Screens.Ranking private void selectedScoreChanged(ValueChangedEvent score) { // avoid contracting panels unnecessarily when TriggerChange is fired manually. - if (!score.OldValue.Equals(score.NewValue)) + if (score.OldValue != null && !score.OldValue.Equals(score.NewValue)) { // Contract the old panel. foreach (var t in flow.Where(t => t.Panel.Score.Equals(score.OldValue))) From 8461eaab46922fb44c937ef62b1469acd7315045 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 14:17:22 +0900 Subject: [PATCH 068/227] `BeatmapSetInfo` detach support --- .../Database/BeatmapImporterTests.cs | 45 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapInfo.cs | 4 ++ osu.Game/Beatmaps/BeatmapManager.cs | 4 +- osu.Game/Beatmaps/BeatmapModelManager.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfo.cs | 4 ++ osu.Game/Beatmaps/WorkingBeatmapCache.cs | 4 ++ osu.Game/Collections/CollectionManager.cs | 2 +- osu.Game/Database/RealmLive.cs | 5 ++- osu.Game/Database/RealmObjectExtensions.cs | 26 +++++++++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++ 10 files changed, 90 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 5b63db1972..78b8628669 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -35,6 +35,51 @@ namespace osu.Game.Tests.Database [TestFixture] public class BeatmapImporterTests : RealmTest { + [Test] + public void TestDetach() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using (var importer = new BeatmapModelManager(realmFactory, storage)) + using (new RulesetStore(realmFactory, storage)) + { + ILive? imported; + + using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) + imported = await importer.Import(reader); + + Assert.NotNull(imported); + Debug.Assert(imported != null); + + BeatmapSetInfo? detached = null; + + imported.PerformRead(live => + { + var timer = new Stopwatch(); + timer.Start(); + detached = live.Detach(); + Logger.Log($"Detach took {timer.ElapsedMilliseconds} ms"); + + Logger.Log($"NamedFiles: {live.Files.Count} {detached.Files.Count}"); + Logger.Log($"Files: {live.Files.Select(f => f.File).Count()} {detached.Files.Select(f => f.File).Count()}"); + Logger.Log($"Difficulties: {live.Beatmaps.Count} {detached.Beatmaps.Count}"); + Logger.Log($"BeatmapDifficulties: {live.Beatmaps.Select(f => f.Difficulty).Count()} {detached.Beatmaps.Select(f => f.Difficulty).Count()}"); + Logger.Log($"Metadata: {live.Metadata} {detached.Metadata}"); + }); + + Logger.Log("Testing detached-ness"); + + Debug.Assert(detached != null); + + Logger.Log($"NamedFiles: {detached.Files.Count}"); + Logger.Log($"Files: {detached.Files.Select(f => f.File).Count()}"); + Logger.Log($"Difficulties: {detached.Beatmaps.Count}"); + Logger.Log($"BeatmapDifficulties: {detached.Beatmaps.Select(f => f.Difficulty).Count()}"); + Logger.Log($"Metadata: {detached.Metadata}"); + } + }); + } + [Test] public void TestImportBeatmapThenCleanup() { diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index ea2508032e..153f40e39d 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using AutoMapper; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Testing; @@ -143,6 +144,7 @@ namespace osu.Game.Beatmaps private int rulesetID; [Ignored] + [IgnoreMap] public int RulesetID { // ReSharper disable once ConstantConditionalAccessQualifier @@ -182,6 +184,8 @@ namespace osu.Game.Beatmaps public BeatmapInfo Clone() => this.Detach(); + public override string ToString() => Metadata?.ToString() ?? base.ToString(); + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d9484a2880..2479390700 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -151,7 +151,7 @@ namespace osu.Game.Beatmaps public List GetAllUsableBeatmapSets() { using (var context = contextFactory.CreateContext()) - return context.All().Where(b => !b.DeletePending).ToList(); + return context.All().Where(b => !b.DeletePending).Detach(); } /// @@ -199,7 +199,7 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapInfo? QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query); + public BeatmapInfo? QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query)?.Detach(); /// /// Saves an file against a given . diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 387521ee1d..254babc3a1 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -107,7 +107,7 @@ namespace osu.Game.Beatmaps public BeatmapInfo? QueryBeatmap(Expression> query) { using (var context = ContextFactory.CreateContext()) - return context.All().FirstOrDefault(query); // TODO: ?.ToLive(); + return context.All().FirstOrDefault(query)?.Detach(); } } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index ed16e3a965..1cc6a96f40 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using AutoMapper; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Extensions; @@ -75,7 +76,10 @@ namespace osu.Game.Beatmaps public bool Equals(IBeatmapSetInfo? other) => other is BeatmapSetInfo b && Equals(b); + [IgnoreMap] IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; + + [IgnoreMap] IEnumerable IHasNamedFiles.Files => Files; } } diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index beec48becd..6be69b5477 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -82,6 +82,10 @@ namespace osu.Game.Beatmaps beatmapInfo = BeatmapManager.QueryBeatmap(b => b.ID == lookupId); } + // TODO: FUCK THE WORLD :D + if (beatmapInfo?.IsManaged == true) + beatmapInfo = beatmapInfo.Detach(); + if (beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index c4f991094c..d230e649f7 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -209,7 +209,7 @@ namespace osu.Game.Collections string checksum = sr.ReadString(); - var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum); + var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum)?.Detach(); if (beatmap != null) collection.Beatmaps.Add(beatmap); } diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 90b8814c24..45bc0e4200 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -61,8 +61,9 @@ namespace osu.Game.Database /// The action to perform. public TReturn PerformRead(Func perform) { - if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) - throw new InvalidOperationException(@$"Realm live objects should not exit the scope of {nameof(PerformRead)}."); + // TODO: this is weird and kinda wrong... unmanaged objects should be allowed? + // if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) + // throw new InvalidOperationException(@$"Realm live objects should not exit the scope of {nameof(PerformRead)}."); if (!IsManaged) return perform(data); diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index e09f046421..9d7d08e106 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -6,7 +6,10 @@ using System.Collections.Generic; using System.Linq; using AutoMapper; using osu.Framework.Development; +using osu.Game.Beatmaps; using osu.Game.Input.Bindings; +using osu.Game.Models; +using osu.Game.Rulesets; using Realms; #nullable enable @@ -18,9 +21,23 @@ namespace osu.Game.Database private static readonly IMapper mapper = new MapperConfiguration(c => { c.ShouldMapField = fi => false; - c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; + c.ShouldMapProperty = pi => true; c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + + c.ForAllMaps((a, b) => + { + b.PreserveReferences(); + b.MaxDepth(2); + }); }).CreateMapper(); /// @@ -32,7 +49,7 @@ namespace osu.Game.Database /// A list of managed s to detach. /// The type of object. /// A list containing non-managed copies of provided items. - public static List Detach(this IEnumerable items) where T : RealmObject + public static List Detach(this IEnumerable items) where T : RealmObjectBase { var list = new List(); @@ -51,7 +68,7 @@ namespace osu.Game.Database /// The managed to detach. /// The type of object. /// A non-managed copy of provided item. Will return the provided item if already detached. - public static T Detach(this T item) where T : RealmObject + public static T Detach(this T item) where T : RealmObjectBase { if (!item.IsManaged) return item; @@ -65,7 +82,8 @@ namespace osu.Game.Database return realmList.Select(l => new RealmLiveUnmanaged(l)).Cast>().ToList(); } - public static ILive ToLiveUnmanaged(this T realmObject) + public static ILive ToLiveUnmanaged(this T realmObject + ) where T : RealmObject, IHasGuidPrimaryKey { return new RealmLiveUnmanaged(realmObject); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 78e416c64b..226d2df619 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -662,6 +662,10 @@ namespace osu.Game.Screens.Select if (beatmapSet.Beatmaps.All(b => b.Hidden)) return null; + // TODO: FUCK THE WORLD :D + if (beatmapSet?.IsManaged == true) + beatmapSet = beatmapSet.Detach(); + // todo: probably not required any more. // foreach (var b in beatmapSet.Beatmaps) // b.Metadata ??= beatmapSet.Metadata; From de3a338d02f2767a6051298790efdd7927262538 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 14:47:03 +0900 Subject: [PATCH 069/227] Update realm queries to use `Filter` to allow for indirect property filtering --- .../Screens/Select/Carousel/TopLocalRank.cs | 20 ++++++++++++------- .../Select/Leaderboards/BeatmapLeaderboard.cs | 18 ++++++++--------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 2ade213e8b..9a0085c62a 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -13,6 +13,7 @@ using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Scoring; +using Realms; namespace osu.Game.Screens.Select.Carousel { @@ -47,13 +48,15 @@ namespace osu.Game.Screens.Select.Carousel { base.LoadComplete(); - scoreSubscription = realmFactory.Context.All().Where(s => s.BeatmapInfo.ID == beatmapInfo.ID).QueryAsyncWithNotifications((_, changes, ___) => - { - if (changes == null) - return; + scoreSubscription = realmFactory.Context.All() + .Filter($"{nameof(ScoreInfo.Beatmap)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) + .QueryAsyncWithNotifications((_, changes, ___) => + { + if (changes == null) + return; - fetchTopScoreRank(); - }); + fetchTopScoreRank(); + }); } private IDisposable scoreSubscription; @@ -84,7 +87,10 @@ namespace osu.Game.Screens.Select.Carousel using (var realm = realmFactory.CreateContext()) { - return realm.All().Where(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) + return realm.All() + .AsEnumerable() + // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). + .Where(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) .OrderByDescending(s => s.TotalScore) .FirstOrDefault() ?.Rank; diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index c2ab930550..f2654cbdee 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -17,6 +17,7 @@ using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; +using Realms; namespace osu.Game.Screens.Select.Leaderboards { @@ -80,9 +81,6 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RealmContextFactory realmContextFactory { get; set; } - [BackgroundDependencyLoader] private void load() { @@ -111,13 +109,15 @@ namespace osu.Game.Screens.Select.Leaderboards if (beatmapInfo == null) return; - scoreSubscription = realmContextFactory.Context.All().Where(s => s.BeatmapInfo.ID == beatmapInfo.ID).QueryAsyncWithNotifications((_, changes, ___) => - { - if (changes == null) - return; + scoreSubscription = realmFactory.Context.All() + .Filter($"{nameof(ScoreInfo.Beatmap)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) + .QueryAsyncWithNotifications((_, changes, ___) => + { + if (changes == null) + return; - RefreshScores(); - }); + RefreshScores(); + }); } protected override void Reset() From e1f77b87def512748cf8cde40c2b82c606545654 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 16:48:49 +0900 Subject: [PATCH 070/227] "Fix" OnlinePlayBeatmapAvailabilityTracker --- .../Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 01999ba766..d344846e5f 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -83,13 +83,10 @@ namespace osu.Game.Online.Rooms progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); }, true); - // These events are needed for a fringe case where a modified/altered beatmap is imported with matching OnlineIDs. - // During the import process this will cause the existing beatmap set to be silently deleted and replaced with the new one. - // This is not exposed to us via `BeatmapDownloadTracker` so we have to take it into our own hands (as we care about the hash matching). realmSubscription?.Dispose(); realmSubscription = realmContextFactory.Context .All() - .Where(s => s.OnlineID == SelectedItem.Value.BeatmapID || (matchingHash != null && s.ID == matchingHash.ID)) + .Where(s => s.OnlineID == SelectedItem.Value.BeatmapID) // TODO: figure out if we need this hash match in the subscription || s.ID == matchingHash?.ID) .QueryAsyncWithNotifications((items, changes, ___) => { if (changes == null) @@ -145,7 +142,13 @@ namespace osu.Game.Online.Rooms int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending); + var foundBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum); + + // can't be included in the above query due to realm limitations. + if (foundBeatmap?.BeatmapSet?.DeletePending == true) + return null; + + return foundBeatmap; } protected override void Dispose(bool isDisposing) From 2d2faa72a96ca601888da4d55d28381ff382f2fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 16:49:03 +0900 Subject: [PATCH 071/227] Fix rulesets being out of order --- osu.Game/Rulesets/RulesetStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 6ec89f9dc5..c675fbbf63 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets List detachedRulesets = new List(); // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. - foreach (var r in rulesets) + foreach (var r in rulesets.OrderBy(r => r.OnlineID)) { try { From 60d2de8a3b486d3b9b85fa726784740732d5d4c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 16:49:13 +0900 Subject: [PATCH 072/227] Fix potential nullref when song select filters to no results --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 54a4c3270d..9ba5e77bbb 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -482,7 +482,7 @@ namespace osu.Game.Screens.Select else selectionChangedDebounce = Scheduler.AddDelayed(run, 200); - if (!beatmap.Equals(beatmapInfoPrevious)) + if (beatmap?.Equals(beatmapInfoPrevious) != true) { if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50) { From 2b8706b6ce2762bf6d1130840308a4bb3f6dd905 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 17:27:48 +0900 Subject: [PATCH 073/227] Detach and reattach scores to make work --- osu.Game/Database/RealmObjectExtensions.cs | 2 ++ osu.Game/OsuGame.cs | 2 ++ .../Scoring/Legacy/DatabasedLegacyScoreDecoder.cs | 4 ++-- osu.Game/Scoring/ScoreInfo.cs | 8 ++++++++ osu.Game/Scoring/ScoreModelManager.cs | 11 ++++++++++- osu.Game/Screens/Play/Player.cs | 12 +++++------- .../Select/Leaderboards/BeatmapLeaderboard.cs | 7 ++++++- 7 files changed, 35 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 9d7d08e106..dfa3076448 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Models; using osu.Game.Rulesets; +using osu.Game.Scoring; using Realms; #nullable enable @@ -27,6 +28,7 @@ namespace osu.Game.Database c.CreateMap(); c.CreateMap(); c.CreateMap(); + c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8d1eccb7c1..149de9bc76 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -502,6 +502,8 @@ namespace osu.Game return; } + databasedScoreInfo = databasedScoreInfo.Detach(); + var databasedScore = ScoreManager.GetScore(databasedScoreInfo); if (databasedScore.Replay == null) diff --git a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs index a49eb89ecc..03e13455f0 100644 --- a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs @@ -21,7 +21,7 @@ namespace osu.Game.Scoring.Legacy this.beatmaps = beatmaps; } - protected override Ruleset GetRuleset(int rulesetId) => rulesets.GetRuleset(rulesetId).CreateInstance(); - protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.MD5Hash == md5Hash)); + protected override Ruleset GetRuleset(int rulesetId) => rulesets.GetRuleset(rulesetId)?.CreateInstance(); + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => b.MD5Hash == md5Hash)); } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 65c31951b8..c07f4a4f87 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using AutoMapper; using Newtonsoft.Json; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -112,9 +113,16 @@ namespace osu.Game.Scoring [MapTo(nameof(Rank))] public int RankInt { get; set; } + [IgnoreMap] IRulesetInfo IScoreInfo.Ruleset => Ruleset; + + [IgnoreMap] IBeatmapInfo IScoreInfo.Beatmap => Beatmap; + + [IgnoreMap] IUser IScoreInfo.User => User; + + [IgnoreMap] IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 81f80df3ad..8f04e50f63 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -55,6 +55,15 @@ namespace osu.Game.Scoring public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); protected override Task Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) - => Task.CompletedTask; + { + // Ensure the beatmap is not detached. + if (!model.Beatmap.IsManaged) + model.Beatmap = realm.Find(model.Beatmap.ID); + + if (!model.Ruleset.IsManaged) + model.Ruleset = realm.Find(model.Ruleset.ShortName); + + return Task.CompletedTask; + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5a443ec48e..9fc0d70cc0 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1038,19 +1038,17 @@ namespace osu.Game.Screens.Play replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } + // the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import. + var importableScore = score.ScoreInfo.DeepClone(); + // For the time being, online ID responses are not really useful for anything. // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores. // // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint // conflicts across various systems (ie. solo and multiplayer). - long onlineScoreId = score.ScoreInfo.OnlineID; + importableScore.OnlineID = -1; - score.ScoreInfo.OnlineID = -1; - - await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false); - - // ... And restore the online ID for other processes to handle correctly (e.g. de-duplication for the results screen). - score.ScoreInfo.OnlineID = onlineScoreId; + await scoreManager.Import(importableScore, replayReader).ConfigureAwait(false); } /// diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index f2654cbdee..caa994a98b 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -149,7 +149,10 @@ namespace osu.Game.Screens.Select.Leaderboards { using (var realm = realmFactory.CreateContext()) { - var scores = realm.All().Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); + var scores = realm.All() + .AsEnumerable() + // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). + .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { @@ -164,6 +167,8 @@ namespace osu.Game.Screens.Select.Leaderboards scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym))); } + scores = scores.Detach(); + scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) .ContinueWith(ordered => scoresCallback?.Invoke(ordered.Result), TaskContinuationOptions.OnlyOnRanToCompletion); From aaefd72c6917a2f497b8883b81116406cd29fc5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 22:57:12 +0900 Subject: [PATCH 074/227] Handle ignored mappings locally in `Detach` configuration --- osu.Game/Beatmaps/BeatmapSetInfo.cs | 3 --- osu.Game/Database/RealmObjectExtensions.cs | 4 +++- osu.Game/Scoring/ScoreInfo.cs | 8 -------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 1cc6a96f40..f6ca184ea3 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using AutoMapper; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Extensions; @@ -76,10 +75,8 @@ namespace osu.Game.Beatmaps public bool Equals(IBeatmapSetInfo? other) => other is BeatmapSetInfo b && Equals(b); - [IgnoreMap] IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; - [IgnoreMap] IEnumerable IHasNamedFiles.Files => Files; } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index dfa3076448..9827ca6edd 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -22,7 +22,9 @@ namespace osu.Game.Database private static readonly IMapper mapper = new MapperConfiguration(c => { c.ShouldMapField = fi => false; - c.ShouldMapProperty = pi => true; + // If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>. + // Takes a bit of effort to determine whether this is the case though, see https://stackoverflow.com/questions/951536/how-do-i-tell-whether-a-type-implements-ilist + c.ShouldMapProperty = pi => pi.GetMethod?.IsPublic == true; c.CreateMap(); c.CreateMap(); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index c07f4a4f87..65c31951b8 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using AutoMapper; using Newtonsoft.Json; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -113,16 +112,9 @@ namespace osu.Game.Scoring [MapTo(nameof(Rank))] public int RankInt { get; set; } - [IgnoreMap] IRulesetInfo IScoreInfo.Ruleset => Ruleset; - - [IgnoreMap] IBeatmapInfo IScoreInfo.Beatmap => Beatmap; - - [IgnoreMap] IUser IScoreInfo.User => User; - - [IgnoreMap] IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages From 6919df18faf5d11a9ff2d05ea3d96c773b43f446 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 22:57:22 +0900 Subject: [PATCH 075/227] Fix incorrect ordering and grouping of difficulties at song select --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 ++ osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index f2700e9053..b2b3b5411c 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Select.Carousel beatmapSet.Beatmaps .Where(b => !b.Hidden) + .OrderBy(b => b.RulesetID) + .ThenBy(b => b.StarRating) .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild); } diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index f2054677b0..619806f96e 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -87,7 +87,8 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.RulesetID) + .Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } } From 52ca6491594f7179c873ab782ea77802a9b5ae9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 23:55:53 +0900 Subject: [PATCH 076/227] Fix results screen test failures due to relation query --- .../Visual/Ranking/TestSceneResultsScreen.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 666cbf02b5..14ceb8f4d4 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -13,8 +13,10 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; +using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; @@ -23,6 +25,7 @@ using osu.Game.Screens.Ranking.Statistics; using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; +using Realms; namespace osu.Game.Tests.Visual.Ranking { @@ -32,13 +35,22 @@ namespace osu.Game.Tests.Visual.Ranking [Resolved] private BeatmapManager beatmaps { get; set; } + [Resolved] + private RealmContextFactory realmContextFactory { get; set; } + protected override void LoadComplete() { base.LoadComplete(); - var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); - if (beatmapInfo != null) - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); + using (var realm = realmContextFactory.CreateContext()) + { + var beatmapInfo = realm.All() + .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0) + .FirstOrDefault(); + + if (beatmapInfo != null) + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); + } } [Test] From 76670a8faa9c964920e4a6277f4f2dd0bad4281b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Jan 2022 23:56:04 +0900 Subject: [PATCH 077/227] Fix `BeatmapDifficultyCache` not working with detached beatmaps --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index f760c25170..981120c5a8 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -136,7 +136,7 @@ namespace osu.Game.Beatmaps var localRulesetInfo = rulesetInfo as RulesetInfo; // Difficulty can only be computed if the beatmap and ruleset are locally available. - if (localBeatmapInfo?.IsManaged != true || localRulesetInfo == null) + if (localBeatmapInfo?.Ruleset == null || localRulesetInfo == null) { // If not, fall back to the existing star difficulty (e.g. from an online source). return Task.FromResult(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0)); From 13401a884667779bfb9da2b37e12163a551b6179 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Jan 2022 00:39:39 +0900 Subject: [PATCH 078/227] Better handle Statistics to avoid losing data --- osu.Game/Scoring/ScoreInfo.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 65c31951b8..6a9805ba6a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -83,21 +83,22 @@ namespace osu.Game.Scoring public RulesetInfo Ruleset { get; set; } = null!; + private Dictionary? statistics; + [Ignored] public Dictionary Statistics { - // TODO: this is dangerous. a get operation may then modify the dictionary, which would be a fresh copy that is not persisted with the model. - // this is already the case in multiple locations. get { - if (string.IsNullOrEmpty(StatisticsJson)) - return new Dictionary(); + if (statistics != null) + return statistics; - return JsonConvert.DeserializeObject>(StatisticsJson) ?? new Dictionary(); + if (!string.IsNullOrEmpty(StatisticsJson)) + statistics = JsonConvert.DeserializeObject>(StatisticsJson); + + return statistics ??= new Dictionary(); } - // .. todo - // ReSharper disable once ValueParameterNotUsed - set => JsonConvert.SerializeObject(StatisticsJson); + set => statistics = value; } [MapTo("Statistics")] From e74a5022c99eb377e6c5010f3d7c7c0385a28c7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Jan 2022 00:40:14 +0900 Subject: [PATCH 079/227] Fix multiple tests via null checks and changing `ToLive` to `Detach` flow --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 5 +++-- .../Skins/TestSceneBeatmapSkinResources.cs | 2 +- .../Visual/Navigation/TestScenePresentBeatmap.cs | 7 +++---- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 1 + osu.Game/OsuGame.cs | 4 ++-- osu.Game/Scoring/ScoreManager.cs | 4 ++-- osu.Game/Scoring/ScoreModelManager.cs | 4 ++++ osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- osu.Game/Stores/BeatmapImporter.cs | 7 +++++++ .../Visual/Multiplayer/TestMultiplayerClient.cs | 12 ++++++------ 11 files changed, 31 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 3e6a877a35..fc7f3229df 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; @@ -142,7 +143,7 @@ namespace osu.Game.Tests.Scores.IO var scoreManager = osu.Dependencies.Get(); beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))!.Value); - Assert.That(scoreManager.Query(s => s.Equals(imported)).Value.DeletePending, Is.EqualTo(true)); + Assert.That(scoreManager.Query(s => s.Equals(imported)).DeletePending, Is.EqualTo(true)); var secondImport = await LoadScoreIntoOsu(osu, imported); Assert.That(secondImport, Is.Null); @@ -187,7 +188,7 @@ namespace osu.Game.Tests.Scores.IO var scoreManager = osu.Dependencies.Get(); await scoreManager.Import(score, archive); - return scoreManager.Query(_ => true).Value; + return scoreManager.Query(_ => true).Detach(); } internal class TestArchiveReader : ArchiveReader diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index c20ab84a68..98732bfb58 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Skins private void load() { var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).GetResultSafely(); - beatmap = beatmaps.GetWorkingBeatmap(imported.Value.Beatmaps[0]); + beatmap = beatmaps.GetWorkingBeatmap(imported?.Value.Beatmaps[0]); beatmap.LoadTrack(); } diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 81aacf61cd..69b30ec6a0 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -97,7 +97,6 @@ namespace osu.Game.Tests.Visual.Navigation BeatmapSetInfo imported = null; AddStep($"import beatmap {i}", () => { - var difficulty = new BeatmapDifficulty(); var metadata = new BeatmapMetadata { Artist = "SomeArtist", @@ -115,18 +114,18 @@ namespace osu.Game.Tests.Visual.Navigation { OnlineID = i * 1024, Metadata = metadata, - BaseDifficulty = difficulty, + BaseDifficulty = new BeatmapDifficulty(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, new BeatmapInfo { OnlineID = i * 2048, Metadata = metadata, - BaseDifficulty = difficulty, + BaseDifficulty = new BeatmapDifficulty(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, } - }).GetResultSafely().Value; + }).GetResultSafely()?.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index b1414d5896..cbcfc07202 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("store selection", () => selection = carousel.SelectedBeatmapInfo); if (isIterating) - AddUntilStep("selection changed", () => !carousel.SelectedBeatmapInfo.Equals(selection)); + AddUntilStep("selection changed", () => !carousel.SelectedBeatmapInfo?.Equals(selection) == true); else AddUntilStep("selection not changed", () => carousel.SelectedBeatmapInfo.Equals(selection)); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2479390700..4f6b74ba0a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -200,6 +200,7 @@ namespace osu.Game.Beatmaps /// The query. /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query)?.Detach(); + // TODO: move detach to usages? /// /// Saves an file against a given . diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 149de9bc76..b9e0aac6ef 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -491,10 +491,10 @@ namespace osu.Game ScoreInfo databasedScoreInfo = null; if (score.OnlineID > 0) - databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID)?.Value; + databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID); if (score is ScoreInfo scoreInfo) - databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash)?.Value; + databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash); if (databasedScoreInfo == null) { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 159c3c2da0..62e61d3a94 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -49,10 +49,10 @@ namespace osu.Game.Scoring /// /// The query. /// The first result for the provided query, or null if no results were found. - public ILive Query(Expression> query) + public ScoreInfo Query(Expression> query) { using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(contextFactory); + return context.All().FirstOrDefault(query)?.Detach(); } /// diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 8f04e50f63..27b02fc2f0 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -63,6 +64,9 @@ namespace osu.Game.Scoring if (!model.Ruleset.IsManaged) model.Ruleset = realm.Find(model.Ruleset.ShortName); + if (string.IsNullOrEmpty(model.StatisticsJson)) + model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); + return Task.CompletedTask; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 226d2df619..ffb5c6a2a5 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Select newRoot.AddChildren(beatmapSets.Select(createCarouselSet).Where(g => g != null)); root = newRoot; - if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) + if (selectedBeatmapSet != null && (!selectedBeatmapSet.BeatmapSet.IsManaged || !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))) selectedBeatmapSet = null; Scroll.Clear(false); diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 2c73bd22eb..e0eee84456 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -59,8 +59,15 @@ namespace osu.Game.Stores beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm)); foreach (BeatmapInfo b in beatmapSet.Beatmaps) + { b.BeatmapSet = beatmapSet; + // ensure we aren't trying to add a new ruleset to the database + // this can happen in tests, mostly + if (!b.Ruleset.IsManaged) + b.Ruleset = realm.Find(b.Ruleset.ShortName); + } + validateOnlineIds(beatmapSet, realm); bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 76acac3f8b..15ede6cc26 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -409,18 +409,18 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default) { - IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) - .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet - ?? beatmaps.QueryBeatmap(b => b.OnlineID == beatmapId)?.BeatmapSet; + IBeatmapInfo? beatmap = roomManager.ServerSideRooms.SelectMany(r => r.Playlist) + .FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value + ?? beatmaps.QueryBeatmap(b => b.OnlineID == beatmapId); - if (set == null) + if (beatmap == null) throw new InvalidOperationException("Beatmap not found."); return Task.FromResult(new APIBeatmap { - BeatmapSet = new APIBeatmapSet { OnlineID = set.OnlineID }, + BeatmapSet = new APIBeatmapSet { OnlineID = beatmap.BeatmapSet?.OnlineID ?? -1 }, OnlineID = beatmapId, - Checksum = set.Beatmaps.First(b => b.OnlineID == beatmapId).MD5Hash + Checksum = beatmap.MD5Hash }); } From b619ff126434b0e37356b8608afb82b5cc5a478a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Jan 2022 14:03:04 +0900 Subject: [PATCH 080/227] Reattach detached items on delete/undelete --- osu.Game/Stores/RealmArchiveModelManager.cs | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 8698799b4a..c53348fd92 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -156,19 +156,31 @@ namespace osu.Game.Stores public bool Delete(TModel item) { - if (item.DeletePending) - return false; + using (var realm = ContextFactory.CreateContext()) + { + if (!item.IsManaged) + item = realm.Find(item.ID); - item.Realm.Write(r => item.DeletePending = true); - return true; + if (item?.DeletePending != false) + return false; + + realm.Write(r => item.DeletePending = true); + return true; + } } public void Undelete(TModel item) { - if (!item.DeletePending) - return; + using (var realm = ContextFactory.CreateContext()) + { + if (!item.IsManaged) + item = realm.Find(item.ID); - item.Realm.Write(r => item.DeletePending = false); + if (item?.DeletePending != true) + return; + + realm.Write(r => item.DeletePending = false); + } } // TODO: delete or abstract From a3c70ccdfc15b02e4db479ae06857d2be3133e8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Jan 2022 14:03:23 +0900 Subject: [PATCH 081/227] Fix OnlineAvailabilityTracker referencing a value in query that could potentially be null --- osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index d344846e5f..1832c116bc 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -86,7 +86,7 @@ namespace osu.Game.Online.Rooms realmSubscription?.Dispose(); realmSubscription = realmContextFactory.Context .All() - .Where(s => s.OnlineID == SelectedItem.Value.BeatmapID) // TODO: figure out if we need this hash match in the subscription || s.ID == matchingHash?.ID) + .Where(s => s.OnlineID == item.NewValue.BeatmapID) // TODO: figure out if we need this hash match in the subscription || s.ID == matchingHash?.ID) .QueryAsyncWithNotifications((items, changes, ___) => { if (changes == null) From ba4ef0926fa21ebce350cc5689772be459d0584f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Jan 2022 15:26:44 +0900 Subject: [PATCH 082/227] Remove incorrect test fixture specification --- osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs index 8e67ecd818..44f6943871 100644 --- a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs +++ b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs @@ -15,7 +15,6 @@ using osu.Game.Tests.Resources; namespace osu.Game.Tests.Beatmaps.IO { - [TestFixture] public static class BeatmapImportHelper { public static async Task LoadQuickOszIntoOsu(OsuGameBase osu) From f2f1adb792c8931b30298b62199c3e439b8b5128 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 12:28:09 +0900 Subject: [PATCH 083/227] Update `FilterMatchingTest` and filter code to use ruleset's `OnlineID` --- osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 8ba3d1a6c7..3d78043c73 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -17,7 +17,6 @@ namespace osu.Game.Tests.NonVisual.Filtering private BeatmapInfo getExampleBeatmap() => new BeatmapInfo { Ruleset = new RulesetInfo { OnlineID = 0 }, - RulesetID = 0, StarRating = 4.0d, BaseDifficulty = new BeatmapDifficulty { From e5af673b01866d6b197bb71817ef2eb1234d8af1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 12:36:11 +0900 Subject: [PATCH 084/227] Fix incorrect `BeatmapInfo.ToString` implementation --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 153f40e39d..8bfcd947ac 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -184,7 +184,7 @@ namespace osu.Game.Beatmaps public BeatmapInfo Clone() => this.Detach(); - public override string ToString() => Metadata?.ToString() ?? base.ToString(); + public override string ToString() => this.GetDisplayTitle(); #endregion } From 7e7784b78a616d5128f9e03e03e8c7473d74e155 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 12:39:49 +0900 Subject: [PATCH 085/227] Fix incorrect access to `ILive` in `BeatmapSkinResources` tests --- osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 98732bfb58..fe0423dcfc 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -26,8 +26,12 @@ namespace osu.Game.Tests.Skins private void load() { var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).GetResultSafely(); - beatmap = beatmaps.GetWorkingBeatmap(imported?.Value.Beatmaps[0]); - beatmap.LoadTrack(); + + imported?.PerformRead(s => + { + beatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]); + beatmap.LoadTrack(); + }); } [Test] From 62517137961b606739e46477bce55393105b1d1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 12:56:25 +0900 Subject: [PATCH 086/227] Add missing `Ruleset` in `ReplayRecorder` tests --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index e6361a15d7..4eab1a21da 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -64,7 +64,11 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } + ScoreInfo = + { + BeatmapInfo = gameplayState.Beatmap.BeatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), From fa7dddcf3c1e3af3d280a58d751b9c50e2ce3ff7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:03:25 +0900 Subject: [PATCH 087/227] Fix `TestScenePresentScore` sharing metadata/difficulty across multiple beatmaps --- .../Navigation/TestScenePresentScore.cs | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index dbff5964df..f9649db135 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -28,14 +29,6 @@ namespace osu.Game.Tests.Visual.Navigation { AddStep("import beatmap", () => { - var difficulty = new BeatmapDifficulty(); - var metadata = new BeatmapMetadata - { - Artist = "SomeArtist", - AuthorString = "SomeAuthor", - Title = "import" - }; - beatmap = Game.BeatmapManager.Import(new BeatmapSetInfo { Hash = Guid.NewGuid().ToString(), @@ -45,19 +38,29 @@ namespace osu.Game.Tests.Visual.Navigation new BeatmapInfo { OnlineID = 1 * 1024, - Metadata = metadata, - BaseDifficulty = difficulty, + Metadata = new BeatmapMetadata + { + Artist = "SomeArtist", + AuthorString = "SomeAuthor", + Title = "import" + }, + BaseDifficulty = new BeatmapDifficulty(), Ruleset = new OsuRuleset().RulesetInfo }, new BeatmapInfo { OnlineID = 1 * 2048, - Metadata = metadata, - BaseDifficulty = difficulty, + Metadata = new BeatmapMetadata + { + Artist = "SomeArtist", + AuthorString = "SomeAuthor", + Title = "import" + }, + BaseDifficulty = new BeatmapDifficulty(), Ruleset = new OsuRuleset().RulesetInfo }, } - }).GetResultSafely().Value; + }).GetResultSafely()?.Value; }); } @@ -130,7 +133,8 @@ namespace osu.Game.Tests.Visual.Navigation Hash = Guid.NewGuid().ToString(), OnlineID = i, BeatmapInfo = beatmap.Beatmaps.First(), - Ruleset = ruleset ?? new OsuRuleset().RulesetInfo + Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, + User = new GuestUser(), }).GetResultSafely().Value; }); From 82259ee0720df42422ee42e21c2bd553d41c9038 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:08:44 +0900 Subject: [PATCH 088/227] Improve legibility of `RulesetInfo.Equals` --- osu.Game/Rulesets/RulesetInfo.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 99b4f5915f..5d5cbe6328 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -47,7 +47,11 @@ namespace osu.Game.Rulesets public bool Available { get; set; } - public bool Equals(RulesetInfo? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + public bool Equals(RulesetInfo? other) => other != null + && OnlineID == other.OnlineID + && Available == other.Available + && Name == other.Name + && InstantiationInfo == other.InstantiationInfo; public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); From 5dd0bb12183dd065e01be932aca007e0faf503b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:08:57 +0900 Subject: [PATCH 089/227] Ensure `Score` created by `GameplayState` has a valid ruleset --- osu.Game/Screens/Play/GameplayState.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 44f72022f7..83881f739d 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -50,7 +50,13 @@ namespace osu.Game.Screens.Play { Beatmap = beatmap; Ruleset = ruleset; - Score = score ?? new Score(); + Score = score ?? new Score + { + ScoreInfo = + { + Ruleset = ruleset.RulesetInfo + } + }; Mods = mods ?? ArraySegment.Empty; } From c831e9107a09472bce2f6f5e90b63901fd107df5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:16:39 +0900 Subject: [PATCH 090/227] Fix `BeatmapInfo.Clone` potentially not cloning if already detached --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 8bfcd947ac..cea5f4531d 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -182,7 +182,7 @@ namespace osu.Game.Beatmaps public int BeatmapVersion; - public BeatmapInfo Clone() => this.Detach(); + public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone(); public override string ToString() => this.GetDisplayTitle(); From 83ccbc1d132aff271899922e2c69d80be97d24ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:25:23 +0900 Subject: [PATCH 091/227] Mention safety failures of Beatmap/Score constructors --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index cea5f4531d..32eff68d5c 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps } [UsedImplicitly] - public BeatmapInfo() + public BeatmapInfo() // TODO: this bypasses null safeties. needs to be hidden from user api (only should be used by realm). { } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6a9805ba6a..cb81901d0a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -44,6 +45,18 @@ namespace osu.Game.Scoring [MapTo("User")] public RealmUser RealmUser { get; set; } = null!; + public ScoreInfo(BeatmapInfo beatmap, RulesetInfo ruleset, RealmUser user) + { + Ruleset = ruleset; + Beatmap = beatmap; + RealmUser = user; + } + + [UsedImplicitly] + public ScoreInfo() // TODO: this bypasses null safeties. needs to be hidden from user api (only should be used by realm). + { + } + // TODO: this is a bit temporary to account for the fact that this class is used to ferry API user data to certain UI components. // Eventually we should either persist enough information to realm to not require the API lookups, or perform the API lookups locally. private APIUser? user; From d19a9a0ba32b6a0cf42455b6abbf227615aeaaf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:37:53 +0900 Subject: [PATCH 092/227] Remove assertion of `ScoreInfo.Combo` being database persisted --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index fc7f3229df..a09acbc8a7 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -50,7 +50,6 @@ namespace osu.Game.Tests.Scores.IO Assert.AreEqual(toImport.TotalScore, imported.TotalScore); Assert.AreEqual(toImport.Accuracy, imported.Accuracy); Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); - Assert.AreEqual(toImport.Combo, imported.Combo); Assert.AreEqual(toImport.User.Username, imported.User.Username); Assert.AreEqual(toImport.Date, imported.Date); Assert.AreEqual(toImport.OnlineID, imported.OnlineID); From 463a185605be7dd03488ea3e844f7c1bbaacb5c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:38:12 +0900 Subject: [PATCH 093/227] Fix many instances of `User` being null in score import tests --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index a09acbc8a7..1fa2a363b9 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -72,6 +72,7 @@ namespace osu.Game.Tests.Scores.IO var toImport = new ScoreInfo { + User = new APIUser { Username = "Test user" }, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; @@ -98,6 +99,7 @@ namespace osu.Game.Tests.Scores.IO var toImport = new ScoreInfo { + User = new APIUser { Username = "Test user" }, Statistics = new Dictionary { { HitResult.Perfect, 100 }, @@ -128,6 +130,7 @@ namespace osu.Game.Tests.Scores.IO var toImport = new ScoreInfo { + User = new APIUser { Username = "Test user" }, Hash = Guid.NewGuid().ToString(), Statistics = new Dictionary { @@ -163,12 +166,20 @@ namespace osu.Game.Tests.Scores.IO { var osu = LoadOsuIntoHost(host, true); - await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineID = 2 }, new TestArchiveReader()); + await LoadScoreIntoOsu(osu, new ScoreInfo + { + User = new APIUser { Username = "Test user" }, + OnlineID = 2 + }, new TestArchiveReader()); var scoreManager = osu.Dependencies.Get(); // Note: A new score reference is used here since the import process mutates the original object to set an ID - Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineID = 2 })); + Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo + { + User = new APIUser { Username = "Test user" }, + OnlineID = 2 + })); } finally { From 33b5fa347375e3285e2b9752aeae685e9ee98a50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:38:23 +0900 Subject: [PATCH 094/227] Detach score during import tests to ensure original object doesn't get managed --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 1fa2a363b9..e14159da8e 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -192,6 +192,8 @@ namespace osu.Game.Tests.Scores.IO { var beatmapManager = osu.Dependencies.Get(); + // clone to avoid attaching the input score to realm. + score = score.DeepClone(); score.BeatmapInfo ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); score.Ruleset ??= new OsuRuleset().RulesetInfo; From ba62d2c756168940154faa9c7a2beb68ba7ac98f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:38:37 +0900 Subject: [PATCH 095/227] Fix `ScoreInfo` oversights causing automapper to fail Parameter in ctor *has* to be named `realmUser` else automapper will try to map to the `User` property. --- osu.Game/Scoring/ScoreInfo.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index cb81901d0a..28a6d67b6a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using AutoMapper; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Localisation; @@ -45,11 +46,11 @@ namespace osu.Game.Scoring [MapTo("User")] public RealmUser RealmUser { get; set; } = null!; - public ScoreInfo(BeatmapInfo beatmap, RulesetInfo ruleset, RealmUser user) + public ScoreInfo(BeatmapInfo beatmap, RulesetInfo ruleset, RealmUser realmUser) { Ruleset = ruleset; Beatmap = beatmap; - RealmUser = user; + RealmUser = realmUser; } [UsedImplicitly] @@ -61,6 +62,7 @@ namespace osu.Game.Scoring // Eventually we should either persist enough information to realm to not require the API lookups, or perform the API lookups locally. private APIUser? user; + [IgnoreMap] public APIUser User { get => user ??= new APIUser @@ -164,7 +166,6 @@ namespace osu.Game.Scoring [Ignored] public bool Passed { get; set; } = true; - [Ignored] public int Combo { get; set; } /// From 43c7b0d2c8a31df449c1042bf721f8e79c0880e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:53:38 +0900 Subject: [PATCH 096/227] Fix unsupported realm operations in multiple tests --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 4 ++-- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index e14159da8e..a1d47351dd 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -144,8 +144,8 @@ namespace osu.Game.Tests.Scores.IO var beatmapManager = osu.Dependencies.Get(); var scoreManager = osu.Dependencies.Get(); - beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))!.Value); - Assert.That(scoreManager.Query(s => s.Equals(imported)).DeletePending, Is.EqualTo(true)); + beatmapManager.Delete(beatmapManager.GetAllUsableBeatmapSets().First(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))); + Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); var secondImport = await LoadScoreIntoOsu(osu, imported); Assert.That(secondImport, Is.Null); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 7b81f330d0..396bbc3dc5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -796,8 +796,8 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestChangeRulesetWhilePresentingScore() { - BeatmapInfo getPresentBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 0); - BeatmapInfo getSwitchBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 1); + BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 0); + BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 1); changeRuleset(0); @@ -828,8 +828,8 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestChangeBeatmapWhilePresentingScore() { - BeatmapInfo getPresentBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 0); - BeatmapInfo getSwitchBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 1); + BeatmapInfo getPresentBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 0); + BeatmapInfo getSwitchBeatmap() => manager.GetAllUsableBeatmapSets().Where(s => !s.DeletePending).SelectMany(s => s.Beatmaps).First(b => b.RulesetID == 1); changeRuleset(0); From 47390d7ec32e8d58a4b32985172010ba2a74dad1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:59:33 +0900 Subject: [PATCH 097/227] Update handling of ruleset nullability when handling a game-wide change --- osu.Game/OsuGameBase.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ac04be532a..d18ae6d2d8 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -424,9 +424,21 @@ namespace osu.Game private void onRulesetChanged(ValueChangedEvent r) { - Ruleset instance; + Ruleset instance = null; - if (r.NewValue?.Available != true || (instance = r.NewValue.CreateInstance()) == null) + try + { + if (r.NewValue?.Available == true) + { + instance = r.NewValue.CreateInstance(); + } + } + catch (Exception e) + { + Logger.Error(e, "Ruleset load failed and has been rolled back"); + } + + if (instance == null) { // reject the change if the ruleset is not available. Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First(); From d8e75a9de49168249a0bd478043e2f3228d26cc8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 13:59:46 +0900 Subject: [PATCH 098/227] Reimplmeent `IsAvailableLocally` as an `abstract` method --- osu.Game/Scoring/ScoreModelManager.cs | 6 ++++++ osu.Game/Skinning/SkinModelManager.cs | 2 ++ osu.Game/Stores/BeatmapImporter.cs | 6 ++++++ osu.Game/Stores/RealmArchiveModelManager.cs | 3 +-- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 27b02fc2f0..1068aba231 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -69,5 +69,11 @@ namespace osu.Game.Scoring return Task.CompletedTask; } + + public override bool IsAvailableLocally(ScoreInfo model) + { + using (var context = ContextFactory.CreateContext()) + return context.All().Any(b => b.OnlineID == model.OnlineID); + } } } diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index 822cb8efa0..66a1808dc1 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -262,5 +262,7 @@ namespace osu.Game.Skinning s.Hash = ComputeHash(s); }); } + + public override bool IsAvailableLocally(SkinInfo model) => false; } } diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index e0eee84456..429d3951cf 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -170,6 +170,12 @@ namespace osu.Game.Stores return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds); } + public override bool IsAvailableLocally(BeatmapSetInfo model) + { + using (var context = ContextFactory.CreateContext()) + return context.All().Any(b => b.OnlineID == model.OnlineID); + } + public override string HumanisedModelName => "beatmap"; protected override BeatmapSetInfo? CreateModel(ArchiveReader reader) diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index c53348fd92..0b78071d16 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -183,8 +183,7 @@ namespace osu.Game.Stores } } - // TODO: delete or abstract - public virtual bool IsAvailableLocally(TModel model) => false; // Not relevant for skins since they can't be downloaded yet. + public abstract bool IsAvailableLocally(TModel model); public void Update(TModel skin) { From af5d3af664a84e20967b80ca6fb1d446261b0730 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 14:08:00 +0900 Subject: [PATCH 099/227] Remove test coverage of scores being deleted when beatmaps are This is not supported in realm for now. Probably best suited to a separate pass, similar to files, using backlink count. --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 38 --------------------- 1 file changed, 38 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index a1d47351dd..22b792cee4 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -119,44 +119,6 @@ namespace osu.Game.Tests.Scores.IO } } - [Test] - public async Task TestImportWithDeletedBeatmapSet() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) - { - try - { - var osu = LoadOsuIntoHost(host, true); - - var toImport = new ScoreInfo - { - User = new APIUser { Username = "Test user" }, - Hash = Guid.NewGuid().ToString(), - Statistics = new Dictionary - { - { HitResult.Perfect, 100 }, - { HitResult.Miss, 50 } - } - }; - - var imported = await LoadScoreIntoOsu(osu, toImport); - - var beatmapManager = osu.Dependencies.Get(); - var scoreManager = osu.Dependencies.Get(); - - beatmapManager.Delete(beatmapManager.GetAllUsableBeatmapSets().First(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))); - Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); - - var secondImport = await LoadScoreIntoOsu(osu, imported); - Assert.That(secondImport, Is.Null); - } - finally - { - host.Exit(); - } - } - } - [Test] public async Task TestOnlineScoreIsAvailableLocally() { From ae8f522c20682cb08ef04d8fdf5fc2e47b135082 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 14:18:34 +0900 Subject: [PATCH 100/227] Add support for persisting score's mods to realm --- osu.Game/Scoring/ScoreInfo.cs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 28a6d67b6a..eb4a0d21d7 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -183,6 +183,10 @@ namespace osu.Game.Scoring [Ignored] public bool IsLegacyScore => Mods.OfType().Any(); + // Used for database serialisation/deserialisation. + [MapTo("Mods")] + public string ModsJson { get; set; } = string.Empty; + [Ignored] public Mod[] Mods { @@ -203,6 +207,7 @@ namespace osu.Game.Scoring { localAPIMods = null; mods = value; + ModsJson = JsonConvert.SerializeObject(APIMods); } } @@ -212,13 +217,18 @@ namespace osu.Game.Scoring { get { - if (localAPIMods != null) - return localAPIMods; + if (localAPIMods == null) + { + // prioritise reading from realm backing + if (!string.IsNullOrEmpty(ModsJson)) + localAPIMods = JsonConvert.DeserializeObject(ModsJson); - if (mods == null) - return Array.Empty(); + // then check mods set via Mods property. + if (mods != null) + localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); + } - return localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); + return localAPIMods ?? Array.Empty(); } set { @@ -226,6 +236,7 @@ namespace osu.Game.Scoring // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. mods = null; + ModsJson = JsonConvert.SerializeObject(APIMods); } } From f4a1fa85a12c5267f5443b7c780a153891276277 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 14:36:05 +0900 Subject: [PATCH 101/227] Fix incorrect conditional for deciding whether scores can be deleted from UI --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 10 +++++----- .../Match/Components/MatchLeaderboardScore.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 3eb9a02a5c..247a509aa1 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Leaderboards protected Container RankContainer { get; private set; } private readonly int? rank; - private readonly bool allowHighlight; + private readonly bool isOnlineScope; private Box background; private Container content; @@ -68,12 +68,12 @@ namespace osu.Game.Online.Leaderboards [Resolved] private Storage storage { get; set; } - public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true) + public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true) { Score = score; this.rank = rank; - this.allowHighlight = allowHighlight; + this.isOnlineScope = isOnlineScope; RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -111,7 +111,7 @@ namespace osu.Game.Online.Leaderboards background = new Box { RelativeSizeAxes = Axes.Both, - Colour = user.OnlineID == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black, + Colour = user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black, Alpha = background_alpha, }, }, @@ -399,7 +399,7 @@ namespace osu.Game.Online.Leaderboards if (Score.Files.Count > 0) items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score))); - if (Score.IsManaged) + if (!isOnlineScope) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); return items.ToArray(); diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs index e8f5b1e826..799c44cc28 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs @@ -14,8 +14,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components { private readonly APIUserScoreAggregate score; - public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool allowHighlight = true) - : base(score.CreateScoreInfo(), rank, allowHighlight) + public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool isOnlineScope = true) + : base(score.CreateScoreInfo(), rank, isOnlineScope) { this.score = score; } From 8ecfb9172ef5d87703f2ee36bb91a5ac121927ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 14:36:21 +0900 Subject: [PATCH 102/227] Fix multiple tests with incorrect access to beatmap imports --- .../Menus/TestSceneMusicActionHandling.cs | 5 ++- .../TestSceneDeleteLocalScore.cs | 35 +++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index ee9363fa12..3ebc64cd0b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -39,7 +39,10 @@ namespace osu.Game.Tests.Visual.Menus AddStep("import beatmap with track", () => { var setWithTrack = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely(); - Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Value.Beatmaps.First()); + setWithTrack?.PerformRead(s => + { + Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(s.Beatmaps.First()); + }); }); AddStep("bind to track change", () => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 49200f6e1a..17168bf07f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -22,6 +22,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Tests.Resources; @@ -90,23 +91,29 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); - beatmapInfo = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely().Value.Beatmaps[0]; + var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); - for (int i = 0; i < 50; i++) + imported?.PerformRead(s => { - var score = new ScoreInfo - { - OnlineID = i, - BeatmapInfo = beatmapInfo, - Accuracy = RNG.NextDouble(), - TotalScore = RNG.Next(1, 1000000), - MaxCombo = RNG.Next(1, 1000), - Rank = ScoreRank.XH, - User = new APIUser { Username = "TestUser" }, - }; + beatmapInfo = s.Beatmaps[0].Detach(); - importedScores.Add(scoreManager.Import(score).GetResultSafely().Value); - } + for (int i = 0; i < 50; i++) + { + var score = new ScoreInfo + { + OnlineID = i, + BeatmapInfo = beatmapInfo, + Accuracy = RNG.NextDouble(), + TotalScore = RNG.Next(1, 1000000), + MaxCombo = RNG.Next(1, 1000), + Rank = ScoreRank.XH, + User = new APIUser { Username = "TestUser" }, + Ruleset = new OsuRuleset().RulesetInfo, + }; + + importedScores.Add(scoreManager.Import(score).GetResultSafely().Value); + } + }); return dependencies; } From a0f8debafe909bb8aa8bbb6ab965d7eaa3b33e4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 14:40:41 +0900 Subject: [PATCH 103/227] Add note about `BeatmapMetadata.Author` being weird --- osu.Game/Beatmaps/BeatmapMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index f729b55154..6349476550 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -27,7 +27,7 @@ namespace osu.Game.Beatmaps [JsonProperty("artist_unicode")] public string ArtistUnicode { get; set; } = string.Empty; - public RealmUser Author { get; set; } = new RealmUser(); + public RealmUser Author { get; set; } = new RealmUser(); // TODO: not sure we want to initialise this only to have it overwritten by retrieval. public string Source { get; set; } = string.Empty; From 41d90cd0b5a2f1689d03263f9c44fd64693088ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 14:52:59 +0900 Subject: [PATCH 104/227] Fix beatmap carousel test failures --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 3 +-- osu.Game/Screens/Select/BeatmapCarousel.cs | 12 +++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index cbcfc07202..d7e3951ac2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -698,10 +698,9 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 1; i <= 15; i++) { - set.Beatmaps.Add(new BeatmapInfo + set.Beatmaps.Add(new BeatmapInfo(new OsuRuleset().RulesetInfo, new BeatmapDifficulty(), new BeatmapMetadata()) { DifficultyName = $"Stars: {i}", - Ruleset = new OsuRuleset().RulesetInfo, StarRating = i, }); } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ffb5c6a2a5..25f48ae455 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -98,10 +98,16 @@ namespace osu.Game.Screens.Select private IEnumerable beatmapSets => root.Children.OfType(); // todo: only used for testing, maybe remove. + private bool loadedTestBeatmaps; + public IEnumerable BeatmapSets { get => beatmapSets.Select(g => g.BeatmapSet); - set => loadBeatmapSets(value); + set + { + loadedTestBeatmaps = true; + loadBeatmapSets(value); + } } private void loadBeatmapSets(IEnumerable beatmapSets) @@ -190,6 +196,10 @@ namespace osu.Game.Screens.Select private void beatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) { + // If loading test beatmaps, avoid overwriting with realm subscription callbacks. + if (loadedTestBeatmaps) + return; + if (changes == null) { // initial load From 4b690703b3142b1db78c6296d8e60b4598fef294 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 14:59:15 +0900 Subject: [PATCH 105/227] Remove unnecessary DI dependencies from cache test --- osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index 99904af42d..e17e4a88da 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -24,7 +24,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Skinning; -using osu.Game.Stores; using osu.Game.Utils; namespace osu.Game.Tests.Visual.Navigation @@ -69,7 +68,6 @@ namespace osu.Game.Tests.Visual.Navigation typeof(ISkinSource), typeof(IAPIProvider), typeof(RulesetStore), - typeof(RealmFileStore), typeof(ScoreManager), typeof(BeatmapManager), typeof(IRulesetConfigCache), From 02d0ca274189e02a8e6da0eb277cbe860025fba7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 14:59:25 +0900 Subject: [PATCH 106/227] Fix protected beatmaps showing up in the song select carousel --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 25f48ae455..f1ad7db9b5 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -190,7 +190,7 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - subscriptionSets = realmFactory.Context.All().Where(s => !s.DeletePending).QueryAsyncWithNotifications(beatmapSetsChanged); + subscriptionSets = realmFactory.Context.All().Where(s => !s.DeletePending && !s.Protected).QueryAsyncWithNotifications(beatmapSetsChanged); subscriptionBeatmaps = realmFactory.Context.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); } From 605898ec53c217e682450fb32e99a44d94d0aa6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 15:06:41 +0900 Subject: [PATCH 107/227] Add missing "non-null" elements missing from some tests --- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- .../Gameplay/TestSceneSpectatorPlayback.cs | 9 +++++++- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 21 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index c5f56cae9e..b6a08dd2ab 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestEmptyLegacyBeatmapSkinFallsBack() { - CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); + CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo { Metadata = new BeatmapMetadata() }, null, null)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index f7e9a1fe16..4790bd44db 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -372,7 +372,14 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { public TestReplayRecorder() - : base(new Score { ScoreInfo = { BeatmapInfo = new BeatmapInfo() } }) + : base(new Score + { + ScoreInfo = + { + BeatmapInfo = new BeatmapInfo(), + Ruleset = new OsuRuleset().RulesetInfo, + } + }) { } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index a5f42545c1..78df4d8d98 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -17,6 +17,7 @@ using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; @@ -136,6 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { Id = 6602580, @@ -158,6 +160,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, User = new APIUser { @@ -200,6 +203,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Ruleset = new OsuRuleset().RulesetInfo, BeatmapInfo = beatmapInfo, User = new APIUser { @@ -220,6 +224,7 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { Id = 4608074, @@ -239,6 +244,8 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser { Id = 1014222, @@ -258,6 +265,8 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser { Id = 1541390, @@ -277,6 +286,8 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser { Id = 2243452, @@ -296,6 +307,8 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser { Id = 2705430, @@ -315,6 +328,8 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser { Id = 7151382, @@ -334,6 +349,8 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser { Id = 2051389, @@ -353,6 +370,8 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser { Id = 6169483, @@ -372,6 +391,8 @@ namespace osu.Game.Tests.Visual.SongSelect TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, BeatmapInfo = beatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser { Id = 6702666, From c33e1631789ff4b04d334a2654279c2b3cdbff0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 16:31:28 +0900 Subject: [PATCH 108/227] Bind ruleset to toolbar later for safety --- osu.Game/Overlays/Toolbar/Toolbar.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index dc0b06b255..776f7ad7b7 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -58,8 +58,11 @@ namespace osu.Game.Overlays.Toolbar AlwaysPresent = false; } + [Resolved] + private Bindable ruleset { get; set; } + [BackgroundDependencyLoader(true)] - private void load(OsuGame osuGame, Bindable parentRuleset) + private void load(OsuGame osuGame) { Children = new Drawable[] { @@ -106,13 +109,17 @@ namespace osu.Game.Overlays.Toolbar } }; - // Bound after the selector is added to the hierarchy to give it a chance to load the available rulesets - rulesetSelector.Current.BindTo(parentRuleset); - if (osuGame != null) OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); } + protected override void LoadComplete() + { + base.LoadComplete(); + + rulesetSelector.Current.BindTo(ruleset); + } + public class ToolbarBackground : Container { private readonly Box gradientBackground; From 5cbd73186480ac7a9cb19d5c34cf20431dc201a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 16:31:50 +0900 Subject: [PATCH 109/227] Add `RulesetInfo` hashcode implementation and tidy up equality --- .../TestScenePlayerScoreSubmission.cs | 10 ++++++++- osu.Game/Rulesets/RulesetInfo.cs | 21 ++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 9f34931f54..a4a4f351ec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -255,7 +255,15 @@ namespace osu.Game.Tests.Visual.Gameplay { prepareTestAPI(true); - createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { OnlineID = rulesetId ?? -1 } }); + createPlayerTest(false, createRuleset: () => new OsuRuleset + { + RulesetInfo = + { + Name = "custom", + ShortName = $"custom{rulesetId}", + OnlineID = rulesetId ?? -1 + } + }); AddUntilStep("wait for token request", () => Player.TokenCreationRequested); diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 5d5cbe6328..2e2ec5c024 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -47,14 +47,25 @@ namespace osu.Game.Rulesets public bool Available { get; set; } - public bool Equals(RulesetInfo? other) => other != null - && OnlineID == other.OnlineID - && Available == other.Available - && Name == other.Name - && InstantiationInfo == other.InstantiationInfo; + public bool Equals(RulesetInfo? other) + { + if (ReferenceEquals(this, other)) return true; + if (other == null) return false; + + return ShortName == other.ShortName; + } public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); + public override int GetHashCode() + { + // Importantly, ignore the underlying realm hash code, as it will usually not match. + var hashCode = new HashCode(); + // ReSharper disable once NonReadonlyMemberInGetHashCode + hashCode.Add(ShortName); + return hashCode.ToHashCode(); + } + public override string ToString() => Name; public RulesetInfo Clone() => new RulesetInfo From 8e79898e267c9b4870f2e4b8874dfa4834593f53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 16:34:16 +0900 Subject: [PATCH 110/227] Fix a couple of minor issues with `TestSceneBeatmapRecommendations` --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 08b5802713..b7bc0c37e1 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -113,6 +113,8 @@ namespace osu.Game.Tests.Visual.SongSelect // Switch to catch presentAndConfirm(() => catchSet, 1); + AddAssert("game-wide ruleset changed", () => Game.Ruleset.Value.Equals(catchSet.Beatmaps.First().Ruleset)); + // Present mixed difficulty set, expect current ruleset to be selected presentAndConfirm(() => mixedSet, 2); } @@ -182,7 +184,7 @@ namespace osu.Game.Tests.Visual.SongSelect beatmap.DifficultyName = $"SR{i + 1}"; } - return Game.BeatmapManager.Import(beatmapSet).GetResultSafely().Value; + return Game.BeatmapManager.Import(beatmapSet).GetResultSafely()?.Value; } private bool ensureAllBeatmapSetsImported(IEnumerable beatmapSets) => beatmapSets.All(set => set != null); From 902dc0eaec32ff49aa1893d611ccc68ee4fecb12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 16:34:32 +0900 Subject: [PATCH 111/227] Detach rather than consume live when presenting a beatmap --- osu.Game/OsuGame.cs | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b9e0aac6ef..ad045d08d2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -451,32 +451,31 @@ namespace osu.Game return; } + var detachedSet = databasedSet.PerformRead(s => s.Detach()); + PerformFromScreen(screen => { - databasedSet.PerformRead(set => + // Find beatmaps that match our predicate. + var beatmaps = detachedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); + + // Use all beatmaps if predicate matched nothing + if (beatmaps.Count == 0) + beatmaps = detachedSet.Beatmaps.ToList(); + + // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection. + var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps) + ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) + ?? beatmaps.First(); + + if (screen is IHandlePresentBeatmap presentableScreen) { - // Find beatmaps that match our predicate. - var beatmaps = set.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); - - // Use all beatmaps if predicate matched nothing - if (beatmaps.Count == 0) - beatmaps = set.Beatmaps.ToList(); - - // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection. - var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps) - ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) - ?? beatmaps.First(); - - if (screen is IHandlePresentBeatmap presentableScreen) - { - presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset); - } - else - { - Ruleset.Value = selection.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); - } - }); + presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset); + } + else + { + Ruleset.Value = selection.Ruleset; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); + } }, validScreens: new[] { typeof(SongSelect), typeof(IHandlePresentBeatmap) }); } From b531cd020750e220a8c1fd3914c2efc15af96e81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 16:54:07 +0900 Subject: [PATCH 112/227] Fix donwload trackers not considering deleted scores --- osu.Game/Online/BeatmapDownloadTracker.cs | 2 +- osu.Game/Online/ScoreDownloadTracker.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 7a396ac7aa..be5bdea6f1 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 946a751c31..08362448a3 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); From c15efaeff2256871327360bdb66767455571b99b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 17:07:40 +0900 Subject: [PATCH 113/227] Fix `OnlinePlayBeatmapAvailabilityTracker` not correctly tracking beatmap import changes --- .../Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 1832c116bc..caec944104 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -71,7 +71,7 @@ namespace osu.Game.Online.Rooms AddInternal(downloadTracker); - downloadTracker.State.BindValueChanged(_ => updateAvailability(), true); + downloadTracker.State.BindValueChanged(_ => Scheduler.AddOnce(updateAvailability), true); downloadTracker.Progress.BindValueChanged(_ => { if (downloadTracker.State.Value != DownloadState.Downloading) @@ -85,14 +85,14 @@ namespace osu.Game.Online.Rooms realmSubscription?.Dispose(); realmSubscription = realmContextFactory.Context - .All() - .Where(s => s.OnlineID == item.NewValue.BeatmapID) // TODO: figure out if we need this hash match in the subscription || s.ID == matchingHash?.ID) + .All() + .Where(b => b.OnlineID == item.NewValue.BeatmapID && b.MD5Hash == item.NewValue.Beatmap.Value.MD5Hash) .QueryAsyncWithNotifications((items, changes, ___) => { if (changes == null) return; - Schedule(updateAvailability); + Scheduler.AddOnce(updateAvailability); }); }, true); } From ac3b7aa89364e1efca967118e189a0a7c9f1f6a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 17:21:29 +0900 Subject: [PATCH 114/227] Fix more incorrect test access to `ILive` --- .../TestScenePlaylistsRoomCreation.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 908e93dee2..eacc3d5e31 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Playlists private TestPlaylistsRoomSubScreen match; - private ILive importedBeatmap; + private BeatmapSetInfo importedBeatmap; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Playlists room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); room.Playlist.Add(new PlaylistItem { - Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() }, + Beatmap = { Value = importedBeatmap.Beatmaps.First() }, Ruleset = { Value = new OsuRuleset().RulesetInfo } }); }); @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Playlists room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5); room.Playlist.Add(new PlaylistItem { - Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() }, + Beatmap = { Value = importedBeatmap.Beatmaps.First() }, Ruleset = { Value = new OsuRuleset().RulesetInfo } }); }); @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Host.Value = API.LocalUser.Value; room.Playlist.Add(new PlaylistItem { - Beatmap = { Value = importedBeatmap.Value.Beatmaps.First() }, + Beatmap = { Value = importedBeatmap.Beatmaps.First() }, Ruleset = { Value = new OsuRuleset().RulesetInfo } }); }); @@ -122,9 +122,9 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("store real beatmap values", () => { - realHash = importedBeatmap.Value.Beatmaps[0].MD5Hash; - realOnlineId = importedBeatmap.Value.Beatmaps[0].OnlineID; - realOnlineSetId = importedBeatmap.Value.OnlineID; + realHash = importedBeatmap.Beatmaps[0].MD5Hash; + realOnlineId = importedBeatmap.Beatmaps[0].OnlineID; + realOnlineSetId = importedBeatmap.OnlineID; }); AddStep("import modified beatmap", () => @@ -134,6 +134,7 @@ namespace osu.Game.Tests.Visual.Playlists BeatmapInfo = { OnlineID = realOnlineId, + Metadata = new BeatmapMetadata(), BeatmapSet = { OnlineID = realOnlineSetId @@ -160,6 +161,7 @@ namespace osu.Game.Tests.Visual.Playlists { MD5Hash = realHash, OnlineID = realOnlineId, + Metadata = new BeatmapMetadata(), BeatmapSet = new BeatmapSetInfo { OnlineID = realOnlineSetId, @@ -211,7 +213,7 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); - importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet).GetResultSafely(); + importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet).GetResultSafely()?.Value.Detach(); }); private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen From ca7e11057c871f3e108c172259ff1cbaa2970ec6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 18:19:47 +0900 Subject: [PATCH 115/227] Use better method to ensure online availability tracker is in a clean state --- .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index bdc2317ccd..8c24b2eef8 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -60,9 +60,8 @@ namespace osu.Game.Tests.Online testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapSet = testBeatmapInfo.BeatmapSet; - var existing = beatmaps.QueryBeatmapSet(s => s.OnlineID == testBeatmapSet.OnlineID); - if (existing != null) - beatmaps.Delete(existing.Value); + ContextFactory.Context.Write(r => r.RemoveAll()); + ContextFactory.Context.Write(r => r.RemoveAll()); selectedItem.Value = new PlaylistItem { From a7958b1d317228af103d0648c7de320e1384ab32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 18:26:49 +0900 Subject: [PATCH 116/227] Fix edge cases in online availability tracker and combine query code --- .../OnlinePlayBeatmapAvailabilityTracker.cs | 44 ++++++------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index caec944104..1fa5d15eb3 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -12,6 +12,7 @@ using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Database; +using Realms; namespace osu.Game.Online.Rooms { @@ -28,9 +29,6 @@ namespace osu.Game.Online.Rooms // Required to allow child components to update. Can potentially be replaced with a `CompositeComponent` class if or when we make one. protected override bool RequiresChildrenUpdate => true; - [Resolved] - private BeatmapManager beatmapManager { get; set; } - [Resolved] private RealmContextFactory realmContextFactory { get; set; } = null!; @@ -45,11 +43,6 @@ namespace osu.Game.Online.Rooms private BeatmapDownloadTracker downloadTracker; - /// - /// The beatmap matching the required hash (and providing a final state). - /// - private BeatmapInfo matchingHash; - private IDisposable realmSubscription; protected override void LoadComplete() @@ -83,17 +76,15 @@ namespace osu.Game.Online.Rooms progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); }, true); + // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realmContextFactory.Context - .All() - .Where(b => b.OnlineID == item.NewValue.BeatmapID && b.MD5Hash == item.NewValue.Beatmap.Value.MD5Hash) - .QueryAsyncWithNotifications((items, changes, ___) => - { - if (changes == null) - return; + realmSubscription = filteredBeatmaps().QueryAsyncWithNotifications((items, changes, ___) => + { + if (changes == null) + return; - Scheduler.AddOnce(updateAvailability); - }); + Scheduler.AddOnce(updateAvailability); + }); }, true); } @@ -102,9 +93,6 @@ namespace osu.Game.Online.Rooms if (downloadTracker == null) return; - // will be repopulated below if still valid. - matchingHash = null; - switch (downloadTracker.State.Value) { case DownloadState.NotDownloaded: @@ -120,9 +108,7 @@ namespace osu.Game.Online.Rooms break; case DownloadState.LocallyAvailable: - matchingHash = findMatchingHash(); - - bool hashMatches = matchingHash != null; + bool hashMatches = filteredBeatmaps().Any(); availability.Value = hashMatches ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded(); @@ -137,18 +123,14 @@ namespace osu.Game.Online.Rooms } } - private BeatmapInfo findMatchingHash() + private IQueryable filteredBeatmaps() { int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - var foundBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum); - - // can't be included in the above query due to realm limitations. - if (foundBeatmap?.BeatmapSet?.DeletePending == true) - return null; - - return foundBeatmap; + return realmContextFactory.Context + .All() + .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); } protected override void Dispose(bool isDisposing) From 6033a825ede119066ea24101d76dc6d86250757f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 19:23:43 +0900 Subject: [PATCH 117/227] Ensure `BeatmapInfo` `Difficulty` and `Metadata` is non-null --- osu.Game/Beatmaps/BeatmapInfo.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 32eff68d5c..00d40dc513 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -32,9 +32,21 @@ namespace osu.Game.Beatmaps public RulesetInfo Ruleset { get; set; } = null!; - public BeatmapDifficulty Difficulty { get; set; } = null!; + public BeatmapDifficulty Difficulty { get; set; } = new BeatmapDifficulty(); - public BeatmapMetadata Metadata { get; set; } = null!; + public BeatmapMetadata Metadata { get; set; } = new BeatmapMetadata(); + + public BeatmapInfo(RulesetInfo ruleset, BeatmapDifficulty difficulty, BeatmapMetadata metadata) + { + Ruleset = ruleset; + Difficulty = difficulty; + Metadata = metadata; + } + + [UsedImplicitly] + public BeatmapInfo() // TODO: this bypasses null safeties. needs to be hidden from user api (only should be used by realm). + { + } public BeatmapSetInfo? BeatmapSet { get; set; } @@ -66,18 +78,6 @@ namespace osu.Game.Beatmaps [JsonIgnore] public bool Hidden { get; set; } - public BeatmapInfo(RulesetInfo ruleset, BeatmapDifficulty difficulty, BeatmapMetadata metadata) - { - Ruleset = ruleset; - Difficulty = difficulty; - Metadata = metadata; - } - - [UsedImplicitly] - public BeatmapInfo() // TODO: this bypasses null safeties. needs to be hidden from user api (only should be used by realm). - { - } - #region Properties we may not want persisted (but also maybe no harm?) public double AudioLeadIn { get; set; } From f4515602036311f0413e5ff499ddc0eea65e42f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Jan 2022 19:27:13 +0900 Subject: [PATCH 118/227] Update null allowances across beatmaps and scores --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 00d40dc513..53aba67275 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps } [UsedImplicitly] - public BeatmapInfo() // TODO: this bypasses null safeties. needs to be hidden from user api (only should be used by realm). + public BeatmapInfo() // TODO: consider removing this and migrating all usages to ctor with parameters. { } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index eb4a0d21d7..61df736a4a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -44,7 +44,7 @@ namespace osu.Game.Scoring public long OnlineID { get; set; } = -1; [MapTo("User")] - public RealmUser RealmUser { get; set; } = null!; + public RealmUser RealmUser { get; set; } = new RealmUser(); public ScoreInfo(BeatmapInfo beatmap, RulesetInfo ruleset, RealmUser realmUser) { @@ -54,7 +54,7 @@ namespace osu.Game.Scoring } [UsedImplicitly] - public ScoreInfo() // TODO: this bypasses null safeties. needs to be hidden from user api (only should be used by realm). + public ScoreInfo() // TODO: consider removing this and migrating all usages to ctor with parameters. { } From 58f8aae731f391c7845afefe7083aae770155dab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 13:34:24 +0900 Subject: [PATCH 119/227] Fix one missed instance of `GetResultSafely` --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index caa994a98b..d03a8b645d 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Select.Leaderboards scores = scores.Detach(); scoreManager.OrderByTotalScoreAsync(scores.ToArray(), cancellationToken) - .ContinueWith(ordered => scoresCallback?.Invoke(ordered.Result), TaskContinuationOptions.OnlyOnRanToCompletion); + .ContinueWith(ordered => scoresCallback?.Invoke(ordered.GetResultSafely()), TaskContinuationOptions.OnlyOnRanToCompletion); return null; } From b5975eee3355a39e6988148245a758bb96d63e45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 13:36:58 +0900 Subject: [PATCH 120/227] This file should have been deleted in a previous commit (rebase failure) --- .../Beatmaps/IO/ImportBeatmapTest.cs | 148 ------------------ 1 file changed, 148 deletions(-) delete mode 100644 osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs deleted file mode 100644 index 0d973c8aeb..0000000000 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ /dev/null @@ -1,148 +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 System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Game.Beatmaps; -using osu.Game.Database; -using osu.Game.Scoring; -using osu.Game.Tests.Resources; -using osu.Game.Tests.Scores.IO; - -namespace osu.Game.Tests.Beatmaps.IO -{ - [TestFixture] - public class ImportBeatmapTest : ImportTest - { - public static async Task LoadQuickOszIntoOsu(OsuGameBase osu) - { - string temp = TestResources.GetQuickTestBeatmapForImport(); - - var manager = osu.Dependencies.Get(); - - var importedSet = manager.Import(new ImportTask(temp)).GetResultSafely(); - - ensureLoaded(osu).WaitSafely(); - - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - - return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); - }, TaskCreationOptions.LongRunning); - - public static Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) => Task.Factory.StartNew(() => - { - string temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); - - var manager = osu.Dependencies.Get(); - - var importedSet = manager.Import(new ImportTask(temp)).GetResultSafely(); - - ensureLoaded(osu).WaitSafely(); - - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - - return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); - }, TaskCreationOptions.LongRunning); - - private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) - { - var manager = osu.Dependencies.Get(); - manager.Delete(imported); - - checkBeatmapSetCount(osu, 0); - checkBeatmapSetCount(osu, 1, true); - checkSingleReferencedFileCount(osu, 0); - - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); - } - - private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo) - { - return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo - { - OnlineID = 2, - BeatmapInfo = beatmapInfo, - }, new ImportScoreTest.TestArchiveReader()); - } - - private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false) - { - var manager = osu.Dependencies.Get(); - - Assert.AreEqual(expected, includeDeletePending - ? manager.QueryBeatmapSets(_ => true).ToList().Count - : manager.GetAllUsableBeatmapSets().Count); - } - - private static string hashFile(string filename) - { - using (var s = File.OpenRead(filename)) - return s.ComputeMD5Hash(); - } - - private static void checkBeatmapCount(OsuGameBase osu, int expected) - { - Assert.AreEqual(expected, osu.Dependencies.Get().QueryBeatmaps(_ => true).ToList().Count); - } - - private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected) - { - Assert.AreEqual(expected, osu.Dependencies.Get().Get().FileInfo.Count(f => f.ReferenceCount == 1)); - } - - private static Task ensureLoaded(OsuGameBase osu, int timeout = 60000) => Task.Factory.StartNew(() => - { - IEnumerable resultSets = null; - var store = osu.Dependencies.Get(); - waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineID == 241526)).Any(), - @"BeatmapSet did not import to the database in allocated time.", timeout); - - // ensure we were stored to beatmap database backing... - Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1)."); - IEnumerable queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineID == 241526); - IEnumerable queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineID == 241526); - - // if we don't re-check here, the set will be inserted but the beatmaps won't be present yet. - waitForOrAssert(() => queryBeatmaps().Count() == 12, - @"Beatmaps did not import to the database in allocated time", timeout); - waitForOrAssert(() => queryBeatmapSets().Count() == 1, - @"BeatmapSet did not import to the database in allocated time", timeout); - int countBeatmapSetBeatmaps = 0; - int countBeatmaps = 0; - waitForOrAssert(() => - (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == - (countBeatmaps = queryBeatmaps().Count()), - $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout); - - var set = queryBeatmapSets().First(); - foreach (BeatmapInfo b in set.Beatmaps) - Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID)); - Assert.IsTrue(set.Beatmaps.Count > 0); - var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap; - Assert.IsTrue(beatmap?.HitObjects.Any() == true); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap; - Assert.IsTrue(beatmap?.HitObjects.Any() == true); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap; - Assert.IsTrue(beatmap?.HitObjects.Any() == true); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap; - Assert.IsTrue(beatmap?.HitObjects.Any() == true); - }, TaskCreationOptions.LongRunning); - - private static void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) - { - Task task = Task.Factory.StartNew(() => - { - while (!result()) Thread.Sleep(200); - }, TaskCreationOptions.LongRunning); - - Assert.IsTrue(task.Wait(timeout), failureMessage); - } - } -} From 286994a808b5dc684000b20bdaf76c2f2aebc20f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 14:19:35 +0900 Subject: [PATCH 121/227] Fix `BeatmapDifficulty` cloning regression --- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs | 3 +++ osu.Game/Beatmaps/BeatmapDifficulty.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 9b2e9fedc5..61f932fd98 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -191,6 +191,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps protected override Beatmap CreateBeatmap() => new TaikoBeatmap(); + // Important to note that this is subclassing a realm object. + // Realm doesn't allow this, but for now this can work since we aren't (in theory?) persisting this to the database. + // It is only used during beatmap conversion and processing. internal class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty { public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 90a60c0f0c..f6e8b8c57d 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps /// public BeatmapDifficulty Clone() { - var diff = new BeatmapDifficulty(); + var diff = (BeatmapDifficulty)MemberwiseClone(); CopyTo(diff); return diff; } From 2ce80cc0301b1cfb7da577f564a5d14349261521 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 14:40:47 +0900 Subject: [PATCH 122/227] Add back caching in `WorkingBeatmapCache` --- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 6be69b5477..f4324dcc81 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -12,6 +12,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.Database; @@ -75,21 +76,27 @@ namespace osu.Game.Beatmaps public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) { - // if there are no files, presume the full beatmap info has not yet been fetched from the database. - if (beatmapInfo?.BeatmapSet?.Files.Count == 0) - { - var lookupId = beatmapInfo.ID; - beatmapInfo = BeatmapManager.QueryBeatmap(b => b.ID == lookupId); - } - - // TODO: FUCK THE WORLD :D - if (beatmapInfo?.IsManaged == true) - beatmapInfo = beatmapInfo.Detach(); - if (beatmapInfo?.BeatmapSet == null) return DefaultBeatmap; - return new BeatmapManagerWorkingBeatmap(beatmapInfo, this); + lock (workingCache) + { + var working = workingCache.FirstOrDefault(w => beatmapInfo.Equals(w.BeatmapInfo)); + + if (working != null) + return working; + + // TODO: FUCK THE WORLD :D + if (beatmapInfo?.IsManaged == true) + beatmapInfo = beatmapInfo.Detach(); + + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); + + // best effort; may be higher than expected. + GlobalStatistics.Get("Beatmaps", $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); + + return working; + } } #region IResourceStorageProvider From 9e2ca583a3aa5e04b5fa313fbc7288c00d2c7ae4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 15:33:54 +0900 Subject: [PATCH 123/227] Fix incorrect realm factory isolation in `TestScenePlaySongSelect` --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 396bbc3dc5..3db1d0b25b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -45,7 +45,10 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { + // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. + // At a point we have isolated interactive test runs enough, this can likely be removed. Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(ContextFactory); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); Dependencies.Cache(music = new MusicController()); From cd88ccab4f7486416d25a1235a7ccdf266c4bcc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 15:40:18 +0900 Subject: [PATCH 124/227] Fix `TestScenePlaySongSelect` failure due to detach clone depth --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 3db1d0b25b..6295a52bdd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -640,8 +640,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Get filtered icon", () => { - filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM); - int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); + var selectedSet = songSelect.Carousel.SelectedBeatmapSet; + filteredBeatmap = selectedSet.Beatmaps.First(b => b.BPM < maxBPM); + int filteredBeatmapIndex = getBeatmapIndex(selectedSet, filteredBeatmap); filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); }); From 6613a7e4aebb99a64f3f27bf154ba1605aacf01f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 15:44:33 +0900 Subject: [PATCH 125/227] Fix another case of test ruleset without overriding `ShortName` primary key --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index f196bbd76e..b429619044 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -459,6 +459,8 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestUnimplementedModOsuRuleset : OsuRuleset { + public override string ShortName => "unimplemented"; + public override IEnumerable GetModsFor(ModType type) { if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); From dd19487eb8eab9d13f0fc49e05bdf81ec924c7ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 15:55:13 +0900 Subject: [PATCH 126/227] Fix custom import process in `TestSceneDrawableRoomPlaylist` not working with realm --- .../TestSceneDrawableRoomPlaylist.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index d87a0a4a1c..5bad36c3dd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -153,19 +153,20 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestDownloadButtonHiddenWhenBeatmapExists() { var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo; + ILive imported = null; Debug.Assert(beatmap.BeatmapSet != null); - AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).WaitSafely()); + AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet).GetResultSafely()); - createPlaylistWithBeatmaps(beatmap); + createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach())); assertDownloadButtonVisible(false); - AddStep("delete beatmap set", () => manager.Delete(manager.QueryBeatmapSets(_ => true).Single().Value)); + AddStep("delete beatmap set", () => imported.PerformWrite(s => s.DeletePending = true)); assertDownloadButtonVisible(true); - AddStep("undelete beatmap set", () => manager.Undelete(manager.QueryBeatmapSets(_ => true).Single().Value)); + AddStep("undelete beatmap set", () => imported.PerformWrite(s => s.DeletePending = false)); assertDownloadButtonVisible(false); void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}", @@ -181,7 +182,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var byChecksum = CreateAPIBeatmap(); byChecksum.Checksum = "1337"; // Some random checksum that does not exist locally. - createPlaylistWithBeatmaps(byOnlineId, byChecksum); + createPlaylistWithBeatmaps(() => new[] { byOnlineId, byChecksum }); AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent)); } @@ -195,7 +196,7 @@ namespace osu.Game.Tests.Visual.Multiplayer beatmap.BeatmapSet.HasExplicitContent = true; - createPlaylistWithBeatmaps(beatmap); + createPlaylistWithBeatmaps(() => new[] { beatmap }); } [Test] @@ -327,7 +328,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); } - private void createPlaylistWithBeatmaps(params IBeatmapInfo[] beatmaps) + private void createPlaylistWithBeatmaps(Func> beatmaps) { AddStep("create playlist", () => { @@ -340,7 +341,7 @@ namespace osu.Game.Tests.Visual.Multiplayer int index = 0; - foreach (var b in beatmaps) + foreach (var b in beatmaps()) { playlist.Items.Add(new PlaylistItem { From 64a47ff85066be41c54f58cd7bebff40a9fd0278 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 16:30:35 +0900 Subject: [PATCH 127/227] Allow `RealmArchiveModelManager` file operations to be performed on detached instances --- osu.Game/Stores/RealmArchiveModelManager.cs | 27 +++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 0b78071d16..00fc5bbb6f 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Platform; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Overlays.Notifications; @@ -33,13 +34,29 @@ namespace osu.Game.Stores } public void DeleteFile(TModel item, RealmNamedFileUsage file) => - item.Realm.Write(() => DeleteFile(item, file, item.Realm)); + performFileOperation(item, managed => DeleteFile(managed, file, managed.Realm)); - public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) - => item.Realm.Write(() => ReplaceFile(file, contents, item.Realm)); + public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) => + performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm)); - public void AddFile(TModel item, Stream contents, string filename) - => item.Realm.Write(() => AddFile(item, contents, filename, item.Realm)); + public void AddFile(TModel item, Stream contents, string filename) => + performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm)); + + private void performFileOperation(TModel item, Action operation) + { + // While we are detaching so often, this seems like the easiest way to keep things in sync. + // This method should be removed as soon as all the surrounding pieces support non-detached operations. + if (!item.IsManaged) + { + var managed = ContextFactory.Context.Find(item.ID); + managed.Realm.Write(() => operation(managed)); + + item.Files.Clear(); + item.Files.AddRange(managed.Files.Detach()); + } + else + operation(item); + } /// /// Delete a file from within an ongoing realm transaction. From 8c3dc4333d5ccce809d645ce8817947bd4444920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 16:30:55 +0900 Subject: [PATCH 128/227] Fix incorrect realm access after new beatmap import --- osu.Game/Beatmaps/BeatmapManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4f6b74ba0a..55f05bf05f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -97,9 +97,12 @@ namespace osu.Game.Beatmaps } }; - var imported = beatmapModelManager.Import(set).GetResultSafely()?.Value; + var imported = beatmapModelManager.Import(set).GetResultSafely(); - return GetWorkingBeatmap(imported?.Beatmaps.First()); + if (imported == null) + throw new InvalidOperationException("Failed to import new beatmap"); + + return imported.PerformRead(s => GetWorkingBeatmap(s.Beatmaps.First())); } /// From dc9ea4adeb2d1fbe75ce58c5b75ebb4fa9b86eb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 16:31:00 +0900 Subject: [PATCH 129/227] Remove incorrect test assertion --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 4cc6e2ca18..2386446e96 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -49,7 +49,6 @@ namespace osu.Game.Tests.Visual.Editing public void TestCreateNewBeatmap() { AddStep("save beatmap", () => Editor.Save()); - AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.IsManaged); AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == false); } From 7509a9ff8f6d1a45182eea6fc096135bc89679f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 18:17:13 +0900 Subject: [PATCH 130/227] Update `BeatmapModelManager.Save` to work for editor scenarios --- osu.Game/Beatmaps/BeatmapManager.cs | 7 +++-- osu.Game/Beatmaps/BeatmapModelManager.cs | 36 +++++++++--------------- osu.Game/Screens/Edit/Editor.cs | 3 -- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 55f05bf05f..c869fd1bad 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -82,7 +82,7 @@ namespace osu.Game.Beatmaps } }; - var set = new BeatmapSetInfo + var beatmapSet = new BeatmapSetInfo { Beatmaps = { @@ -97,7 +97,10 @@ namespace osu.Game.Beatmaps } }; - var imported = beatmapModelManager.Import(set).GetResultSafely(); + foreach (BeatmapInfo b in beatmapSet.Beatmaps) + b.BeatmapSet = beatmapSet; + + var imported = beatmapModelManager.Import(beatmapSet).GetResultSafely(); if (imported == null) throw new InvalidOperationException("Failed to import new beatmap"); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 254babc3a1..62d607c4f7 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -70,35 +70,27 @@ namespace osu.Game.Beatmaps stream.Seek(0, SeekOrigin.Begin); - using (var realm = ContextFactory.CreateContext()) - using (var transaction = realm.BeginWrite()) - { - beatmapInfo = setInfo.Beatmaps.Single(b => b.Equals(beatmapInfo)); + // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. + var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); + if (existingFileInfo != null) + DeleteFile(setInfo, existingFileInfo); - // grab the original file (or create a new one if not found). - var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + beatmapInfo.Hash = stream.ComputeSHA2Hash(); - if (existingFileInfo != null) - { - DeleteFile(setInfo, existingFileInfo); - } - - // metadata may have changed; update the path with the standard format. - var metadata = beatmapInfo.Metadata; - string filename = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename(); - - beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - - stream.Seek(0, SeekOrigin.Begin); - AddFile(setInfo, stream, filename, realm); - - transaction.Commit(); - } + AddFile(setInfo, stream, getFilename(beatmapInfo)); + Update(setInfo); } WorkingBeatmapCache?.Invalidate(beatmapInfo); } + private static string getFilename(BeatmapInfo beatmapInfo) + { + var metadata = beatmapInfo.Metadata; + return $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename(); + } + /// /// Perform a lookup query on available s. /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3a420f4994..383b59dc01 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -356,9 +356,6 @@ namespace osu.Game.Screens.Edit // no longer new after first user-triggered save. isNewBeatmap = false; - // apply any set-level metadata changes. - beatmapManager.Update(editorBeatmap.BeatmapInfo.BeatmapSet); - // save the loaded beatmap's data stream. beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); From 80eee6d7b031680a0437943172209e7362248ce5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 18:18:03 +0900 Subject: [PATCH 131/227] Make `RealmArchiveModelManager.Update` work using automapper --- osu.Game/Screens/Menu/IntroScreen.cs | 6 +----- osu.Game/Stores/RealmArchiveModelManager.cs | 7 ++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index e02066560d..159340a4d7 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -123,11 +123,7 @@ namespace osu.Game.Screens.Menu // this could happen if a user has nuked their files store. for now, reimport to repair this. var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely(); - import?.PerformWrite(b => - { - b.Protected = true; - beatmaps.Update(b); - }); + import?.PerformWrite(b => b.Protected = true); loadThemedIntro(); } diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 00fc5bbb6f..7c6f25055f 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -202,8 +202,13 @@ namespace osu.Game.Stores public abstract bool IsAvailableLocally(TModel model); - public void Update(TModel skin) + public void Update(TModel model) { + using (var realm = ContextFactory.CreateContext()) + { + var existing = realm.Find(model.ID); + realm.Write(r => model.CopyChangesToRealm(existing)); + } } } } From f986c3ebd4b0728164e7ad6abf665109e1e4c372 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 18:23:04 +0900 Subject: [PATCH 132/227] Add basic write support via automapper --- osu.Game/Database/RealmObjectExtensions.cs | 36 ++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 9827ca6edd..180e372eba 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -19,7 +19,33 @@ namespace osu.Game.Database { public static class RealmObjectExtensions { - private static readonly IMapper mapper = new MapperConfiguration(c => + private static readonly IMapper write_mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + // If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>. + // Takes a bit of effort to determine whether this is the case though, see https://stackoverflow.com/questions/951536/how-do-i-tell-whether-a-type-implements-ilist + c.ShouldMapProperty = pi => pi.SetMethod?.IsPublic == true; + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); + + c.ForAllMaps((a, b) => + { + b.PreserveReferences(); + b.MaxDepth(2); + }); + }).CreateMapper(); + + private static readonly IMapper read_mapper = new MapperConfiguration(c => { c.ShouldMapField = fi => false; // If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>. @@ -36,6 +62,7 @@ namespace osu.Game.Database c.CreateMap(); c.CreateMap(); c.CreateMap(); + c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); c.ForAllMaps((a, b) => { @@ -77,7 +104,12 @@ namespace osu.Game.Database if (!item.IsManaged) return item; - return mapper.Map(item); + return read_mapper.Map(item); + } + + public static void CopyChangesToRealm(this T source, T destination) where T : RealmObjectBase + { + write_mapper.Map(source, destination); } public static List> ToLiveUnmanaged(this IEnumerable realmList) From b7ee6d186630be137d02074cb02bd24d7cadf012 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 18:33:35 +0900 Subject: [PATCH 133/227] Add protections against test null refs when beatmap load fails --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 3 +++ osu.Game/Tests/Visual/TestPlayer.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index b1f642b909..f27043e5cb 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -389,6 +389,9 @@ namespace osu.Game.Tests.Visual.Background while (BlockLoad && !token.IsCancellationRequested) Thread.Sleep(1); + if (!LoadedBeatmapSuccessfully) + return; + StoryboardEnabled = config.GetBindable(OsuSetting.ShowStoryboard); DrawableRuleset.IsPaused.BindTo(IsPaused); } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index d68984b144..368f792e28 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -94,6 +94,9 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { + if (!LoadedBeatmapSuccessfully) + return; + ScoreProcessor.NewJudgement += r => Results.Add(r); } } From 7dba3c35513d95d5089502f0a81d29a94f8be602 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 19:25:50 +0900 Subject: [PATCH 134/227] Fix most remaining test issues --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 1 + osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs | 1 + osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ---- osu.Game/Stores/RealmArchiveModelManager.cs | 2 +- 6 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index f27043e5cb..3109fd1540 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -50,6 +50,7 @@ namespace osu.Game.Tests.Visual.Background Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); + Dependencies.Cache(ContextFactory); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index dc826701d2..d4282ff21e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -49,6 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); } public override void SetUpSteps() diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 1fa5d15eb3..1f77b1d383 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -90,7 +90,7 @@ namespace osu.Game.Online.Rooms private void updateAvailability() { - if (downloadTracker == null) + if (downloadTracker == null || SelectedItem.Value == null) return; switch (downloadTracker.State.Value) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 159340a4d7..afc5812039 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -223,7 +223,7 @@ namespace osu.Game.Screens.Menu if (!resuming) { beatmap.Value = initialBeatmap; - Track = initialBeatmap.Track; + Track = beatmap.Value.Track; // ensure the track starts at maximum volume musicController.CurrentTrack.FinishTransforms(); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f1ad7db9b5..b002b31c9e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -676,10 +676,6 @@ namespace osu.Game.Screens.Select if (beatmapSet?.IsManaged == true) beatmapSet = beatmapSet.Detach(); - // todo: probably not required any more. - // foreach (var b in beatmapSet.Beatmaps) - // b.Metadata ??= beatmapSet.Metadata; - var set = new CarouselBeatmapSet(beatmapSet) { GetRecommendedBeatmap = beatmaps => GetRecommendedBeatmap?.Invoke(beatmaps) diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 7c6f25055f..3783b10a8c 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -34,7 +34,7 @@ namespace osu.Game.Stores } public void DeleteFile(TModel item, RealmNamedFileUsage file) => - performFileOperation(item, managed => DeleteFile(managed, file, managed.Realm)); + performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm)); public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) => performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm)); From e8dcbaf29ada1f918b57be38d24f1f273d9ea083 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 21:32:01 +0900 Subject: [PATCH 135/227] Fix intro screen hitting null reference if intro beatmap is unavailable --- osu.Game/Overlays/MusicController.cs | 7 ++++--- osu.Game/Screens/Menu/IntroScreen.cs | 18 +++++------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 2731096a00..2497f549b3 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -284,11 +284,12 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Next; - var playable = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault(); + var playableSet = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault(); + var playableBeatmap = playableSet?.Beatmaps?.FirstOrDefault(); - if (playable != null) + if (playableBeatmap != null) { - changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First())); + changeBeatmap(beatmaps.GetWorkingBeatmap(playableBeatmap)); restartTrack(); return true; } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index afc5812039..09a9f44558 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -141,24 +141,13 @@ namespace osu.Game.Screens.Menu if (s.Beatmaps.Count == 0) return; - initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0].ToLive(realmContextFactory)); + initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First()); }); return UsingThemedIntro = initialBeatmap != null; } } - protected override void LoadComplete() - { - base.LoadComplete(); - - // TODO: This is temporary to get the setInfo on the update thread, to make things work "better" without using ILive everywhere. - var setInfo = beatmaps.QueryBeatmapSets(b => b.Hash == BeatmapHash).FirstOrDefault(); - - if (setInfo?.Value.Beatmaps.Count > 0) - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Value.Beatmaps.First()); - } - public override void OnResuming(IScreen last) { this.FadeIn(300); @@ -222,7 +211,10 @@ namespace osu.Game.Screens.Menu if (!resuming) { - beatmap.Value = initialBeatmap; + // generally this can never be null + // an exception is running ruleset tests, where the osu! ruleset may not be prsent (causing importing the intro to fail). + if (initialBeatmap != null) + beatmap.Value = initialBeatmap; Track = beatmap.Value.Track; // ensure the track starts at maximum volume From c06b5951fd159a63b20a36be2367eb119a84f0b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 21:36:34 +0900 Subject: [PATCH 136/227] Fix multiple remaining warnings --- osu.Game/Beatmaps/BeatmapManager.cs | 9 +++++++-- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c869fd1bad..36dcd4f8e8 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -43,11 +43,17 @@ namespace osu.Game.Beatmaps private readonly RealmContextFactory contextFactory; - public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false) + public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false) { this.contextFactory = contextFactory; + if (performOnlineLookups) + { + if (api == null) + throw new ArgumentNullException(nameof(api), "API must be provided if online lookups are required."); + onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + } var userResources = new RealmFileStore(contextFactory, storage).Store; @@ -56,7 +62,6 @@ namespace osu.Game.Beatmaps beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, onlineBeatmapLookupQueue); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); - workingBeatmapCache.BeatmapManager = beatmapModelManager; beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; } diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index f4324dcc81..e1399c5272 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -31,8 +31,6 @@ namespace osu.Game.Beatmaps /// public readonly WorkingBeatmap DefaultBeatmap; - public BeatmapModelManager BeatmapManager { private get; set; } - private readonly AudioManager audioManager; private readonly IResourceStore resources; private readonly LargeTextureStore largeTextureStore; @@ -87,8 +85,7 @@ namespace osu.Game.Beatmaps return working; // TODO: FUCK THE WORLD :D - if (beatmapInfo?.IsManaged == true) - beatmapInfo = beatmapInfo.Detach(); + beatmapInfo = beatmapInfo.Detach(); workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); @@ -193,6 +190,9 @@ namespace osu.Game.Beatmaps { Storyboard storyboard; + if (BeatmapInfo.Path == null) + return new Storyboard(); + try { using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) From 9beabad6a4c98898bae646683b63f07d13b5cf51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 21:41:10 +0900 Subject: [PATCH 137/227] Remove hide/restore event flow --- osu.Game/Beatmaps/BeatmapManager.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 36dcd4f8e8..15dc0c9b61 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -113,16 +113,6 @@ namespace osu.Game.Beatmaps return imported.PerformRead(s => GetWorkingBeatmap(s.Beatmaps.First())); } - /// - /// Fired when a single difficulty has been hidden. - /// - public event Action? BeatmapHidden; - - /// - /// Fired when a single difficulty has been restored. - /// - public event Action? BeatmapRestored; - /// /// Delete a beatmap difficulty. /// @@ -134,8 +124,6 @@ namespace osu.Game.Beatmaps { beatmapInfo.Hidden = true; transaction.Commit(); - - BeatmapHidden?.Invoke(beatmapInfo); } } @@ -150,8 +138,6 @@ namespace osu.Game.Beatmaps { beatmapInfo.Hidden = false; transaction.Commit(); - - BeatmapRestored?.Invoke(beatmapInfo); } } From 46e92c3b601a59d34c85275c74e9af347dd7f3a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 21:44:18 +0900 Subject: [PATCH 138/227] Clean up `BeatmapManager` query methods --- osu.Game/Beatmaps/BeatmapManager.cs | 17 ----------------- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 15dc0c9b61..2cdae2b047 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -151,22 +151,6 @@ namespace osu.Game.Beatmaps return context.All().Where(b => !b.DeletePending).Detach(); } - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// Results from the provided query. - public List> QueryBeatmapSets(Expression> query) - { - using (var context = contextFactory.CreateContext()) - { - return context.All() - .Where(b => !b.DeletePending) - .Where(query) - .ToLive(contextFactory); - } - } - /// /// Perform a lookup query on available s. /// @@ -197,7 +181,6 @@ namespace osu.Game.Beatmaps /// The query. /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query)?.Detach(); - // TODO: move detach to usages? /// /// Saves an file against a given . diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 09a9f44558..c4343625ab 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Menu bool loadThemedIntro() { - setInfo = beatmaps.QueryBeatmapSets(b => b.Hash == BeatmapHash).FirstOrDefault(); + setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); if (setInfo == null) return false; From bf4133021b590c43a142ca9555bc3dbc0982c7e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 22:05:02 +0900 Subject: [PATCH 139/227] Update migration test to use realm file as test --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 4bb54f1625..61ef31e07e 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -179,7 +179,7 @@ namespace osu.Game.Tests.NonVisual { var osu = LoadOsuIntoHost(host); - const string database_filename = "client.db"; + const string database_filename = "client.realm"; Assert.DoesNotThrow(() => osu.Migrate(customPath)); Assert.That(File.Exists(Path.Combine(customPath, database_filename))); From e0c59f4b3ccccb57f7d28488884668de479c7366 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 22:16:06 +0900 Subject: [PATCH 140/227] Localise EF context factory usage to migration only --- .../Visual/Navigation/TestSceneOsuGame.cs | 2 - osu.Game/Database/DatabaseContextFactory.cs | 6 +-- osu.Game/OsuGameBase.cs | 40 +++---------------- 3 files changed, 9 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index e17e4a88da..b8d1636ea0 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -12,7 +12,6 @@ using osu.Framework.Platform; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Input; using osu.Game.Input.Bindings; @@ -57,7 +56,6 @@ namespace osu.Game.Tests.Visual.Navigation private IReadOnlyList requiredGameBaseDependencies => new[] { typeof(OsuGameBase), - typeof(DatabaseContextFactory), typeof(Bindable), typeof(IBindable), typeof(Bindable>), diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 94fa967d72..f79505d7c5 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -13,7 +13,7 @@ namespace osu.Game.Database { private readonly Storage storage; - private const string database_name = @"client.db"; + public const string DATABASE_NAME = @"client.db"; private ThreadLocal threadContexts; @@ -139,7 +139,7 @@ namespace osu.Game.Database threadContexts = new ThreadLocal(CreateContext, true); } - protected virtual OsuDbContext CreateContext() => new OsuDbContext(CreateDatabaseConnectionString(database_name, storage)) + protected virtual OsuDbContext CreateContext() => new OsuDbContext(CreateDatabaseConnectionString(DATABASE_NAME, storage)) { Database = { AutoTransactionsEnabled = false } }; @@ -152,7 +152,7 @@ namespace osu.Game.Database try { - storage.Delete(database_name); + storage.Delete(DATABASE_NAME); } catch { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d18ae6d2d8..a003c6b8ab 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -149,8 +149,6 @@ namespace osu.Game private MultiplayerClient multiplayerClient; - private DatabaseContextFactory contextFactory; - private RealmContextFactory realmFactory; protected override Container Content => content; @@ -186,13 +184,13 @@ namespace osu.Game Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); - dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + DatabaseContextFactory efContextFactory = Storage.Exists(DatabaseContextFactory.DATABASE_NAME) + ? new DatabaseContextFactory(Storage) + : null; - runMigrations(); - - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory)); - - new EFToRealmMigrator(contextFactory, realmFactory, LocalConfig).Run(); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", efContextFactory)); + if (efContextFactory != null) + new EFToRealmMigrator(efContextFactory, realmFactory, LocalConfig).Run(); dependencies.CacheAs(Storage); @@ -397,7 +395,6 @@ namespace osu.Game Scheduler.Add(() => { realmBlocker = realmFactory.BlockAllOperations(); - contextFactory.FlushConnections(); readyToRun.Set(); }, false); @@ -458,29 +455,6 @@ namespace osu.Game AvailableMods.Value = dict; } - private void runMigrations() - { - try - { - using (var db = contextFactory.GetForWrite(false)) - db.Context.Migrate(); - } - catch (Exception e) - { - Logger.Error(e.InnerException ?? e, "Migration failed! We'll be starting with a fresh database.", LoggingTarget.Database); - - // if we failed, let's delete the database and start fresh. - // todo: we probably want a better (non-destructive) migrations/recovery process at a later point than this. - contextFactory.ResetDatabase(); - - Logger.Log("Database purged successfully.", LoggingTarget.Database); - - // only run once more, then hard bail. - using (var db = contextFactory.GetForWrite(false)) - db.Context.Migrate(); - } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -489,8 +463,6 @@ namespace osu.Game BeatmapManager?.Dispose(); LocalConfig?.Dispose(); - contextFactory?.FlushConnections(); - realmFactory?.Dispose(); } } From d5239d550a0a1952591c5cb57f091cf543eb26e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 22:55:00 +0900 Subject: [PATCH 141/227] Add refetch for non-managed hide/restore attempts --- osu.Game/Beatmaps/BeatmapManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2cdae2b047..b0c7efc9d9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -122,6 +122,9 @@ namespace osu.Game.Beatmaps using (var realm = contextFactory.CreateContext()) using (var transaction = realm.BeginWrite()) { + if (!beatmapInfo.IsManaged) + beatmapInfo = realm.Find(beatmapInfo.ID); + beatmapInfo.Hidden = true; transaction.Commit(); } @@ -136,6 +139,9 @@ namespace osu.Game.Beatmaps using (var realm = contextFactory.CreateContext()) using (var transaction = realm.BeginWrite()) { + if (!beatmapInfo.IsManaged) + beatmapInfo = realm.Find(beatmapInfo.ID); + beatmapInfo.Hidden = false; transaction.Commit(); } From 46206f70d6a158b9f4c540cffef656300c5eb106 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 22:57:47 +0900 Subject: [PATCH 142/227] Fix beatmap mass deletion flow --- osu.Game/Beatmaps/BeatmapManager.cs | 13 +++++++++++++ .../Sections/Maintenance/GeneralSettings.cs | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b0c7efc9d9..6f5370a55a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -234,6 +234,19 @@ namespace osu.Game.Beatmaps beatmapModelManager.Delete(items, silent); } + public void Delete(Expression>? filter = null, bool silent = false) + { + using (var context = contextFactory.CreateContext()) + { + var items = context.All().Where(s => !s.DeletePending); + + if (filter != null) + items = items.Where(filter); + + beatmapModelManager.Delete(items.ToList(), silent); + } + } + public void Undelete(List items, bool silent = false) { beatmapModelManager.Undelete(items, silent); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 93884812fe..736ba04e51 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => { deleteBeatmapsButton.Enabled.Value = false; - Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteBeatmapsButton.Enabled.Value = true)); + Task.Run(() => beatmaps.Delete()).ContinueWith(t => Schedule(() => deleteBeatmapsButton.Enabled.Value = true)); })); } }); From 72656ae01e8cc108b5249538f36331d28c063305 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Jan 2022 23:04:36 +0900 Subject: [PATCH 143/227] Fix beatmap restore/undelete flows --- osu.Game/Beatmaps/BeatmapManager.cs | 29 ++++++++++++------- .../Sections/Maintenance/GeneralSettings.cs | 15 ++-------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 6f5370a55a..9b2b7c960d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -147,6 +147,18 @@ namespace osu.Game.Beatmaps } } + public void RestoreAll() + { + using (var realm = contextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + foreach (var beatmap in realm.All().Where(b => b.Hidden)) + beatmap.Hidden = false; + + transaction.Commit(); + } + } + /// /// Returns a list of all usable s. /// @@ -168,17 +180,6 @@ namespace osu.Game.Beatmaps return context.All().FirstOrDefault(query)?.ToLive(contextFactory); } - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// Results from the provided query. - public IQueryable QueryBeatmaps(Expression> query) - { - using (var context = contextFactory.CreateContext()) - return context.All().Where(query); - } - #region Delegation to BeatmapModelManager (methods which previously existed locally). /// @@ -247,6 +248,12 @@ namespace osu.Game.Beatmaps } } + public void UndeleteAll() + { + using (var context = contextFactory.CreateContext()) + beatmapModelManager.Undelete(context.All().Where(s => s.DeletePending).ToList()); + } + public void Undelete(List items, bool silent = false) { beatmapModelManager.Undelete(items, silent); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 736ba04e51..aa02d086f4 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.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.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -106,10 +105,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => { deleteSkinsButton.Enabled.Value = false; - Task.Run(() => - { - skins.Delete(); - }).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true)); + Task.Run(() => skins.Delete()).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true)); })); } }); @@ -147,11 +143,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { restoreButton.Enabled.Value = false; - Task.Run(() => - { - foreach (var b in beatmaps.QueryBeatmaps(b => b.Hidden).ToList()) - beatmaps.Restore(b); - }).ContinueWith(t => Schedule(() => restoreButton.Enabled.Value = true)); + Task.Run(beatmaps.RestoreAll).ContinueWith(t => Schedule(() => restoreButton.Enabled.Value = true)); } }, undeleteButton = new SettingsButton @@ -160,8 +152,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { undeleteButton.Enabled.Value = false; - // TODO: reimplement similar to SkinManager? - // Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); + Task.Run(beatmaps.UndeleteAll).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); } }, }); From 0aff1c232b9581244434410313da2a0d69993557 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 01:03:59 +0900 Subject: [PATCH 144/227] Fix deleted/hidden carousel queries --- osu.Game/Screens/Select/BeatmapCarousel.cs | 40 ++++++++++++++-------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b002b31c9e..f355554482 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -151,7 +151,9 @@ namespace osu.Game.Screens.Select private CarouselRoot root; private IDisposable subscriptionSets; + private IDisposable subscriptionDeletedSets; private IDisposable subscriptionBeatmaps; + private IDisposable subscriptionHiddenBeatmaps; private readonly DrawablePool setPool = new DrawablePool(100); @@ -192,6 +194,24 @@ namespace osu.Game.Screens.Select subscriptionSets = realmFactory.Context.All().Where(s => !s.DeletePending && !s.Protected).QueryAsyncWithNotifications(beatmapSetsChanged); subscriptionBeatmaps = realmFactory.Context.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); + + // Can't use main subscriptions because we can't lookup deleted indices. + // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. + subscriptionDeletedSets = realmFactory.Context.All().Where(s => s.DeletePending && !s.Protected).QueryAsyncWithNotifications(deletedBeatmapSetsChanged); + subscriptionHiddenBeatmaps = realmFactory.Context.All().Where(b => b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); + } + + private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) + { + // If loading test beatmaps, avoid overwriting with realm subscription callbacks. + if (loadedTestBeatmaps) + return; + + if (changes == null) + return; + + foreach (int i in changes.InsertedIndices) + RemoveBeatmapSet(sender[i]); } private void beatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) @@ -210,16 +230,8 @@ namespace osu.Game.Screens.Select foreach (int i in changes.NewModifiedIndices) UpdateBeatmapSet(sender[i]); - // moves also appear as deletes / inserts but aren't important to us. - if (!changes.Moves.Any()) - { - foreach (int i in changes.InsertedIndices) - UpdateBeatmapSet(sender[i]); - - // TODO: This can not work, as we recently found out https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - foreach (int i in changes.DeletedIndices) - RemoveBeatmapSet(sender[i]); - } + foreach (int i in changes.InsertedIndices) + UpdateBeatmapSet(sender[i]); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) @@ -228,9 +240,7 @@ namespace osu.Game.Screens.Select if (changes == null) return; - // TODO: we can probably handle hidden items at a per-panel level (ie. start a realm subscription from there)? - // might be cleaner than handling via reconstruction of the whole set's panels. - foreach (int i in changes.NewModifiedIndices) + foreach (int i in changes.InsertedIndices) UpdateBeatmapSet(sender[i].BeatmapSet); } @@ -673,7 +683,7 @@ namespace osu.Game.Screens.Select return null; // TODO: FUCK THE WORLD :D - if (beatmapSet?.IsManaged == true) + if (beatmapSet.IsManaged) beatmapSet = beatmapSet.Detach(); var set = new CarouselBeatmapSet(beatmapSet) @@ -930,7 +940,9 @@ namespace osu.Game.Screens.Select base.Dispose(isDisposing); subscriptionSets?.Dispose(); + subscriptionDeletedSets?.Dispose(); subscriptionBeatmaps?.Dispose(); + subscriptionHiddenBeatmaps?.Dispose(); } } } From 157dfdaa825f72ea345a5d4c1463ac9639cba364 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 01:18:11 +0900 Subject: [PATCH 145/227] Fix protected beatmap sets getting deleted --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 9b2b7c960d..21cda18c1a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -239,7 +239,7 @@ namespace osu.Game.Beatmaps { using (var context = contextFactory.CreateContext()) { - var items = context.All().Where(s => !s.DeletePending); + var items = context.All().Where(s => !s.DeletePending && !s.Protected); if (filter != null) items = items.Where(filter); From 017285b694bc0ed45697f94d376077fdd0f73db1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 01:18:36 +0900 Subject: [PATCH 146/227] Update `MusicController` to handle deletions more correctly --- osu.Game/Overlays/MusicController.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 2497f549b3..44c2916f12 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -84,12 +84,16 @@ namespace osu.Game.Overlays { base.LoadComplete(); + var availableBeatmaps = realmFactory.Context + .All() + .Where(s => !s.DeletePending); + // ensure we're ready before completing async load. // probably not a good way of handling this (as there is a period we aren't watching for changes until the realm subscription finishes up. - foreach (var s in realmFactory.Context.All()) + foreach (var s in availableBeatmaps) beatmapSets.Add(s); - beatmapSubscription = realmFactory.Context.All().QueryAsyncWithNotifications(beatmapsChanged); + beatmapSubscription = availableBeatmaps.QueryAsyncWithNotifications(beatmapsChanged); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) @@ -98,7 +102,7 @@ namespace osu.Game.Overlays return; foreach (int i in changes.InsertedIndices) - beatmapSets.Insert(i, sender[i]); + beatmapSets.Insert(i, sender[i].Detach()); foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) beatmapSets.RemoveAt(i); @@ -285,7 +289,7 @@ namespace osu.Game.Overlays queuedDirection = TrackChangeDirection.Next; var playableSet = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault(); - var playableBeatmap = playableSet?.Beatmaps?.FirstOrDefault(); + var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); if (playableBeatmap != null) { From dc3730f33413b90789a1e8f5e7742b8ac2e627ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 01:29:40 +0900 Subject: [PATCH 147/227] Fix song select import popup not always showing --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9ba5e77bbb..41700bec17 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -295,7 +295,7 @@ namespace osu.Game.Screens.Select Schedule(() => { // if we have no beatmaps, let's prompt the user to import from over a stable install if he has one. - if (!beatmaps.GetAllUsableBeatmapSets().Any() && DisplayStableImportPrompt) + if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null && DisplayStableImportPrompt) { dialogOverlay.Push(new ImportFromStablePopup(() => { From 6db3c32dd1d81bcc774398fa627be04951d84bfe Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 12 Jan 2022 00:32:32 +0300 Subject: [PATCH 148/227] Handle automapper realm cyclic references via `AfterMap`s This may not be the cleanest solution, but there don't seem to be any way towards this either. - `UseDestinationValue` has been inherited by default as noted in https://docs.automapper.org/en/stable/10.0-Upgrade-Guide.html#usedestinationvalue-is-now-inherited-by-default, and its behaviour in this case would be using the nested **managed** realm object for the destination member rather than creating an unmanaged version. - `MaxDepth` already sets `PreserveReferences` so there's no point of using it. - `MaxDepth` should probably not be set for all maps, only for those with cyclic references, to avoid the expensive overhead of `PreserveReferences`, as mentioned in https://docs.automapper.org/en/stable/5.0-Upgrade-Guide.html#circular-references. That aside, `MaxDepth` should actually only be set to `1` for `BeatmapSetInfo` mapping, because we don't want AutoMapper to create a nested instance of `BeatmapSetInfo` in each mapped/detached beatmap, but for some reason, doing that will cause automapper to not map any beatmap inside the set and leave it with 0 beatmaps. While on the other hand, using `MaxDepth(2)` for `BeatmapSetInfo` works, but creates an unused instance of a `BeatmapSetInfo` inside each mapped beatmap, which may not be ideal. For `BeatmapInfo`, it has to be `MaxDepth(2)`, in which the first `BeatmapInfo` depth would be itself (when detaching a beatmap), and the second would be nested beatmaps inside the mapped/detached `BeatmapSetInfo` within the beatmap. (note that when detaching a beatmap set, the unused instance of `BeatmapSetInfo` within each beatmap of that beatmap set doesn't also have a list of unused beatmaps as one might expect from the depth specification, it surprisingly has 0 beatmaps) This causes it to create an unused instance of `BeatmapInfo` in the beatmap set resembling the root mapped/detached beatmap, but that one might be inevitable. --- osu.Game/Database/RealmObjectExtensions.cs | 44 +++++++++++++++------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 180e372eba..83d23e2fc2 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -34,15 +34,23 @@ namespace osu.Game.Database c.CreateMap(); c.CreateMap(); c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); - - c.ForAllMaps((a, b) => + c.CreateMap().MaxDepth(2).AfterMap((s, d) => { - b.PreserveReferences(); - b.MaxDepth(2); + for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++) + { + if (d.BeatmapSet.Beatmaps[i].Equals(d)) + { + d.BeatmapSet.Beatmaps[i] = d; + break; + } + } }); + c.CreateMap().MaxDepth(2).AfterMap((s, d) => + { + foreach (var beatmap in d.Beatmaps) + beatmap.BeatmapSet = d; + }); + c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); }).CreateMapper(); private static readonly IMapper read_mapper = new MapperConfiguration(c => @@ -60,15 +68,23 @@ namespace osu.Game.Database c.CreateMap(); c.CreateMap(); c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); - - c.ForAllMaps((a, b) => + c.CreateMap().MaxDepth(2).AfterMap((s, d) => { - b.PreserveReferences(); - b.MaxDepth(2); + for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++) + { + if (d.BeatmapSet.Beatmaps[i].Equals(d)) + { + d.BeatmapSet.Beatmaps[i] = d; + break; + } + } }); + c.CreateMap().MaxDepth(2).AfterMap((s, d) => + { + foreach (var beatmap in d.Beatmaps) + beatmap.BeatmapSet = d; + }); + c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); }).CreateMapper(); /// From 51d6db1bca65a02f1d66675f2e766125364d7f1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 12:39:40 +0900 Subject: [PATCH 149/227] Add equatable support to `IUser` and `RealmUser` Not sure this will stick, but let's add it for now to make testing detach support work nicely. --- osu.Game/Beatmaps/IBeatmapMetadataInfo.cs | 2 +- osu.Game/Models/RealmUser.cs | 11 ++++++++++- osu.Game/Users/IUser.cs | 13 ++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs index 968ad14928..61adc0ac34 100644 --- a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs @@ -73,7 +73,7 @@ namespace osu.Game.Beatmaps && TitleUnicode == other.TitleUnicode && Artist == other.Artist && ArtistUnicode == other.ArtistUnicode - && Author == other.Author + && Author.Equals(other.Author) && Source == other.Source && Tags == other.Tags && PreviewTime == other.PreviewTime diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs index 154ece502f..ff35528827 100644 --- a/osu.Game/Models/RealmUser.cs +++ b/osu.Game/Models/RealmUser.cs @@ -1,17 +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 osu.Game.Users; using Realms; namespace osu.Game.Models { - public class RealmUser : EmbeddedObject, IUser + public class RealmUser : EmbeddedObject, IUser, IEquatable { public int OnlineID { get; set; } = 1; public string Username { get; set; } public bool IsBot => false; + + public bool Equals(RealmUser other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return OnlineID == other.OnlineID && Username == other.Username; + } } } diff --git a/osu.Game/Users/IUser.cs b/osu.Game/Users/IUser.cs index 3995531fd9..d9a352872f 100644 --- a/osu.Game/Users/IUser.cs +++ b/osu.Game/Users/IUser.cs @@ -1,14 +1,25 @@ // 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 osu.Game.Database; +#nullable enable + namespace osu.Game.Users { - public interface IUser : IHasOnlineID + public interface IUser : IHasOnlineID, IEquatable { string Username { get; } bool IsBot { get; } + + bool IEquatable.Equals(IUser? other) + { + if (other == null) + return false; + + return OnlineID == other.OnlineID && Username == other.Username; + } } } From 509301d94f86aaa3ca4649bf2334417235b32b88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 12:40:09 +0900 Subject: [PATCH 150/227] Update detach test to assert correct behaviour --- .../Database/BeatmapImporterTests.cs | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 78b8628669..49f992139c 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -43,39 +43,35 @@ namespace osu.Game.Tests.Database using (var importer = new BeatmapModelManager(realmFactory, storage)) using (new RulesetStore(realmFactory, storage)) { - ILive? imported; + ILive? beatmapSet; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) - imported = await importer.Import(reader); + beatmapSet = await importer.Import(reader); - Assert.NotNull(imported); - Debug.Assert(imported != null); + Assert.NotNull(beatmapSet); + Debug.Assert(beatmapSet != null); - BeatmapSetInfo? detached = null; + BeatmapSetInfo? detachedBeatmapSet = null; - imported.PerformRead(live => + beatmapSet.PerformRead(live => { - var timer = new Stopwatch(); - timer.Start(); - detached = live.Detach(); - Logger.Log($"Detach took {timer.ElapsedMilliseconds} ms"); + detachedBeatmapSet = live.Detach(); - Logger.Log($"NamedFiles: {live.Files.Count} {detached.Files.Count}"); - Logger.Log($"Files: {live.Files.Select(f => f.File).Count()} {detached.Files.Select(f => f.File).Count()}"); - Logger.Log($"Difficulties: {live.Beatmaps.Count} {detached.Beatmaps.Count}"); - Logger.Log($"BeatmapDifficulties: {live.Beatmaps.Select(f => f.Difficulty).Count()} {detached.Beatmaps.Select(f => f.Difficulty).Count()}"); - Logger.Log($"Metadata: {live.Metadata} {detached.Metadata}"); + Assert.AreEqual(live.Files.Count, detachedBeatmapSet.Files.Count); + Assert.AreEqual(live.Files.Select(f => f.File).Count(), detachedBeatmapSet.Files.Select(f => f.File).Count()); + Assert.AreEqual(live.Beatmaps.Count, detachedBeatmapSet.Beatmaps.Count); + Assert.AreEqual(live.Beatmaps.Select(f => f.Difficulty).Count(), detachedBeatmapSet.Beatmaps.Select(f => f.Difficulty).Count()); + Assert.AreEqual(live.Metadata, detachedBeatmapSet.Metadata); }); - Logger.Log("Testing detached-ness"); + Debug.Assert(detachedBeatmapSet != null); - Debug.Assert(detached != null); - - Logger.Log($"NamedFiles: {detached.Files.Count}"); - Logger.Log($"Files: {detached.Files.Select(f => f.File).Count()}"); - Logger.Log($"Difficulties: {detached.Beatmaps.Count}"); - Logger.Log($"BeatmapDifficulties: {detached.Beatmaps.Select(f => f.Difficulty).Count()}"); - Logger.Log($"Metadata: {detached.Metadata}"); + // Check detached instances can all be accessed without throwing. + Assert.NotNull(detachedBeatmapSet.Files.Count); + Assert.NotZero(detachedBeatmapSet.Files.Select(f => f.File).Count()); + Assert.NotNull(detachedBeatmapSet.Beatmaps.Count); + Assert.NotZero(detachedBeatmapSet.Beatmaps.Select(f => f.Difficulty).Count()); + Assert.NotNull(detachedBeatmapSet.Metadata); } }); } From c92aff8d2b3ef101358943367bb8806f950ae3c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 12:42:45 +0900 Subject: [PATCH 151/227] Add test of cyclic beatmap/beatmapset references --- osu.Game.Tests/Database/BeatmapImporterTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 49f992139c..a4dd341226 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Database public class BeatmapImporterTests : RealmTest { [Test] - public void TestDetach() + public void TestDetachBeatmapSet() { RunTestWithRealmAsync(async (realmFactory, storage) => { @@ -71,7 +71,11 @@ namespace osu.Game.Tests.Database Assert.NotZero(detachedBeatmapSet.Files.Select(f => f.File).Count()); Assert.NotNull(detachedBeatmapSet.Beatmaps.Count); Assert.NotZero(detachedBeatmapSet.Beatmaps.Select(f => f.Difficulty).Count()); + Assert.NotNull(detachedBeatmapSet.Beatmaps.First().Path); Assert.NotNull(detachedBeatmapSet.Metadata); + + // Check cyclic reference to beatmap set + Assert.AreEqual(detachedBeatmapSet, detachedBeatmapSet.Beatmaps.First().BeatmapSet); } }); } From 580ad03f8d66492f7cd0382dbb372a77e14b1247 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 12:56:28 +0900 Subject: [PATCH 152/227] Combine mapper configurations and add more explanation about special cases --- osu.Game/Database/RealmObjectExtensions.cs | 41 +++------------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 83d23e2fc2..a5e5962a04 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -19,43 +19,11 @@ namespace osu.Game.Database { public static class RealmObjectExtensions { - private static readonly IMapper write_mapper = new MapperConfiguration(c => + private static readonly IMapper mapper = new MapperConfiguration(c => { c.ShouldMapField = fi => false; - // If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>. - // Takes a bit of effort to determine whether this is the case though, see https://stackoverflow.com/questions/951536/how-do-i-tell-whether-a-type-implements-ilist - c.ShouldMapProperty = pi => pi.SetMethod?.IsPublic == true; - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap().MaxDepth(2).AfterMap((s, d) => - { - for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++) - { - if (d.BeatmapSet.Beatmaps[i].Equals(d)) - { - d.BeatmapSet.Beatmaps[i] = d; - break; - } - } - }); - c.CreateMap().MaxDepth(2).AfterMap((s, d) => - { - foreach (var beatmap in d.Beatmaps) - beatmap.BeatmapSet = d; - }); - c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); - }).CreateMapper(); - - private static readonly IMapper read_mapper = new MapperConfiguration(c => - { - c.ShouldMapField = fi => false; + // This is specifically to avoid mapping explicit interface implementations. // If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>. // Takes a bit of effort to determine whether this is the case though, see https://stackoverflow.com/questions/951536/how-do-i-tell-whether-a-type-implements-ilist c.ShouldMapProperty = pi => pi.GetMethod?.IsPublic == true; @@ -84,7 +52,6 @@ namespace osu.Game.Database foreach (var beatmap in d.Beatmaps) beatmap.BeatmapSet = d; }); - c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); }).CreateMapper(); /// @@ -120,12 +87,12 @@ namespace osu.Game.Database if (!item.IsManaged) return item; - return read_mapper.Map(item); + return mapper.Map(item); } public static void CopyChangesToRealm(this T source, T destination) where T : RealmObjectBase { - write_mapper.Map(source, destination); + mapper.Map(source, destination); } public static List> ToLiveUnmanaged(this IEnumerable realmList) From a307f7e90eb5cbf050b0fc0a2dff9f860a51c1c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 12:56:42 +0900 Subject: [PATCH 153/227] Add test coverage of updating via copying changes from detached instance --- .../Database/BeatmapImporterTests.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index a4dd341226..0644313bf9 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -80,6 +80,48 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestUpdateDetachedBeatmapSet() + { + RunTestWithRealmAsync(async (realmFactory, storage) => + { + using (var importer = new BeatmapModelManager(realmFactory, storage)) + using (new RulesetStore(realmFactory, storage)) + { + ILive? beatmapSet; + + using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) + beatmapSet = await importer.Import(reader); + + Assert.NotNull(beatmapSet); + Debug.Assert(beatmapSet != null); + + BeatmapSetInfo? detachedBeatmapSet = null; + + beatmapSet.PerformRead(s => detachedBeatmapSet = s.Detach()); + + Debug.Assert(detachedBeatmapSet != null); + + var newUser = new RealmUser { Username = "peppy", OnlineID = 2 }; + + detachedBeatmapSet.Beatmaps.First().Metadata.Artist = "New Artist"; + detachedBeatmapSet.Beatmaps.First().Metadata.Author = newUser; + + Assert.AreNotEqual(detachedBeatmapSet.Status, BeatmapOnlineStatus.Ranked); + detachedBeatmapSet.Status = BeatmapOnlineStatus.Ranked; + + beatmapSet.PerformWrite(s => detachedBeatmapSet.CopyChangesToRealm(s)); + + beatmapSet.PerformRead(s => + { + Assert.AreEqual(BeatmapOnlineStatus.Ranked, s.Status); + Assert.AreEqual("New Artist", s.Beatmaps.First().Metadata.Artist); + Assert.AreEqual(newUser, s.Beatmaps.First().Metadata.Author); + }); + } + }); + } + [Test] public void TestImportBeatmapThenCleanup() { From a4de0f93fa1a813aa3b9e4782a0302c4a6b4390f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 14:38:37 +0900 Subject: [PATCH 154/227] Move manager `Update` methods to be explicit to where they are still used by legacy code Also fixes skin hash repopulation being completely broken. --- osu.Game/Beatmaps/BeatmapManager.cs | 5 ----- osu.Game/Beatmaps/BeatmapModelManager.cs | 9 +++++++++ osu.Game/Database/IModelManager.cs | 7 ------- osu.Game/Scoring/ScoreManager.cs | 5 ----- osu.Game/Skinning/SkinModelManager.cs | 4 ++-- osu.Game/Stores/RealmArchiveModelManager.cs | 9 --------- 6 files changed, 11 insertions(+), 28 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 21cda18c1a..f901b0fbc1 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -220,11 +220,6 @@ namespace osu.Game.Beatmaps return beatmapModelManager.IsAvailableLocally(model); } - public void Update(BeatmapSetInfo item) - { - beatmapModelManager.Update(item); - } - public bool Delete(BeatmapSetInfo item) { return beatmapModelManager.Delete(item); diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 62d607c4f7..73ada09ef6 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -101,5 +101,14 @@ namespace osu.Game.Beatmaps using (var context = ContextFactory.CreateContext()) return context.All().FirstOrDefault(query)?.Detach(); } + + public void Update(BeatmapSetInfo item) + { + using (var realm = ContextFactory.CreateContext()) + { + var existing = realm.Find(item.ID); + realm.Write(r => item.CopyChangesToRealm(existing)); + } + } } } diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index e7218b621c..187ac86a59 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -12,13 +12,6 @@ namespace osu.Game.Database public interface IModelManager where TModel : class { - /// - /// Perform an update of the specified item. - /// TODO: Support file additions/removals. - /// - /// The item to update. - void Update(TModel item); - /// /// Delete an item from the manager. /// Is a no-op for already deleted items. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 62e61d3a94..ce5d2acc21 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -251,11 +251,6 @@ namespace osu.Game.Scoring #region Implementation of IModelManager - public void Update(ScoreInfo item) - { - scoreModelManager.Update(item); - } - public bool Delete(ScoreInfo item) { return scoreModelManager.Delete(item); diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index 66a1808dc1..1689a89b43 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -210,13 +210,13 @@ namespace osu.Game.Skinning { using (var realm = ContextFactory.CreateContext()) { - var skinsWithoutHashes = realm.All().Where(i => string.IsNullOrEmpty(i.Hash)).ToArray(); + var skinsWithoutHashes = realm.All().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray(); foreach (SkinInfo skin in skinsWithoutHashes) { try { - Update(skin); + checkSkinIniMetadata(skin, realm); } catch (Exception e) { diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 3783b10a8c..4365fdab1e 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -201,14 +201,5 @@ namespace osu.Game.Stores } public abstract bool IsAvailableLocally(TModel model); - - public void Update(TModel model) - { - using (var realm = ContextFactory.CreateContext()) - { - var existing = realm.Find(model.ID); - realm.Write(r => model.CopyChangesToRealm(existing)); - } - } } } From f24b2b1be3c408e035aef9dd13ded223cd0f0ac6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 14:38:58 +0900 Subject: [PATCH 155/227] Make copying detached changes to realm only exposed for `BeatmapSet` Also fixes remaining issues with the copy process. --- .../Database/BeatmapImporterTests.cs | 14 ++++- osu.Game/Beatmaps/BeatmapInfo.cs | 1 + osu.Game/Database/RealmObjectExtensions.cs | 61 +++++++++++++++++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 0644313bf9..1bba73bea0 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -110,13 +110,25 @@ namespace osu.Game.Tests.Database Assert.AreNotEqual(detachedBeatmapSet.Status, BeatmapOnlineStatus.Ranked); detachedBeatmapSet.Status = BeatmapOnlineStatus.Ranked; - beatmapSet.PerformWrite(s => detachedBeatmapSet.CopyChangesToRealm(s)); + beatmapSet.PerformWrite(s => + { + detachedBeatmapSet.CopyChangesToRealm(s); + }); beatmapSet.PerformRead(s => { + // Check above changes explicitly. Assert.AreEqual(BeatmapOnlineStatus.Ranked, s.Status); Assert.AreEqual("New Artist", s.Beatmaps.First().Metadata.Artist); Assert.AreEqual(newUser, s.Beatmaps.First().Metadata.Author); + Assert.NotZero(s.Files.Count); + + // Check nothing was lost in the copy operation. + Assert.AreEqual(s.Files.Count, detachedBeatmapSet.Files.Count); + Assert.AreEqual(s.Files.Select(f => f.File).Count(), detachedBeatmapSet.Files.Select(f => f.File).Count()); + Assert.AreEqual(s.Beatmaps.Count, detachedBeatmapSet.Beatmaps.Count); + Assert.AreEqual(s.Beatmaps.Select(f => f.Difficulty).Count(), detachedBeatmapSet.Beatmaps.Select(f => f.Difficulty).Count()); + Assert.AreEqual(s.Metadata, detachedBeatmapSet.Metadata); }); } }); diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 53aba67275..1b732b3c8a 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -162,6 +162,7 @@ namespace osu.Game.Beatmaps public Guid BeatmapSetInfoID => BeatmapSet?.ID ?? Guid.Empty; [Ignored] + [IgnoreMap] public BeatmapDifficulty BaseDifficulty { get => Difficulty; diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index a5e5962a04..188d619627 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -19,6 +19,50 @@ namespace osu.Game.Database { public static class RealmObjectExtensions { + private static readonly IMapper write_mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + c.ShouldMapProperty = pi => pi.SetMethod?.IsPublic == true; + + c.CreateMap() + .ForMember(s => s.Author, cc => cc.Ignore()) + .AfterMap((s, d) => + { + copyChangesToRealm(s.Author, d.Author); + }); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap() + .ForMember(s => s.Ruleset, cc => cc.Ignore()) + .ForMember(s => s.Metadata, cc => cc.Ignore()) + .ForMember(s => s.Difficulty, cc => cc.Ignore()) + .ForMember(s => s.BeatmapSet, cc => cc.Ignore()) + .AfterMap((s, d) => + { + d.Ruleset = d.Realm.Find(s.Ruleset.ShortName); + copyChangesToRealm(s.Difficulty, d.Difficulty); + copyChangesToRealm(s.Metadata, d.Metadata); + }); + c.CreateMap() + .ForMember(s => s.Beatmaps, cc => cc.Ignore()) + .AfterMap((s, d) => + { + foreach (var beatmap in s.Beatmaps) + { + var existing = d.Beatmaps.FirstOrDefault(b => b.ID == beatmap.ID); + + if (existing != null) + copyChangesToRealm(beatmap, existing); + else + d.Beatmaps.Add(beatmap); + } + }); + + c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); + }).CreateMapper(); + private static readonly IMapper mapper = new MapperConfiguration(c => { c.ShouldMapField = fi => false; @@ -52,6 +96,8 @@ namespace osu.Game.Database foreach (var beatmap in d.Beatmaps) beatmap.BeatmapSet = d; }); + + c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); }).CreateMapper(); /// @@ -90,10 +136,17 @@ namespace osu.Game.Database return mapper.Map(item); } - public static void CopyChangesToRealm(this T source, T destination) where T : RealmObjectBase - { - mapper.Map(source, destination); - } + /// + /// Copy changes in a detached beatmap back to realm. + /// This is a temporary method to handle existing flows only. It should not be used going forward if we can avoid it. + /// + /// The detached beatmap to copy from. + /// The live beatmap to copy to. + public static void CopyChangesToRealm(this BeatmapSetInfo source, BeatmapSetInfo destination) + => copyChangesToRealm(source, destination); + + private static void copyChangesToRealm(T source, T destination) where T : RealmObjectBase + => write_mapper.Map(source, destination); public static List> ToLiveUnmanaged(this IEnumerable realmList) where T : RealmObject, IHasGuidPrimaryKey From 5f7365e8f39e510021a9d123c19892d09e2a65a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 15:09:56 +0900 Subject: [PATCH 156/227] Ensure scores are cleaned up alongside beatmap so they don't have a null reference --- osu.Game/Beatmaps/BeatmapInfo.cs | 5 +++++ osu.Game/Database/RealmContextFactory.cs | 14 ++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 1b732b3c8a..8a2ee95020 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -11,6 +11,7 @@ using osu.Game.Database; using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; +using osu.Game.Scoring; using Realms; #nullable enable @@ -36,6 +37,10 @@ namespace osu.Game.Beatmaps public BeatmapMetadata Metadata { get; set; } = new BeatmapMetadata(); + [IgnoreMap] + [Backlink(nameof(ScoreInfo.Beatmap))] + public IQueryable Scores { get; } = null!; + public BeatmapInfo(RulesetInfo ruleset, BeatmapDifficulty difficulty, BeatmapMetadata metadata) { Ruleset = ruleset; diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 968caaa7ce..1aa6b2043c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -113,12 +113,18 @@ namespace osu.Game.Database { var pendingDeleteSets = realm.All().Where(s => s.DeletePending); - foreach (var s in pendingDeleteSets) + foreach (var beatmapSet in pendingDeleteSets) { - foreach (var b in s.Beatmaps) - realm.Remove(b); + foreach (var beatmap in beatmapSet.Beatmaps) + { + // Cascade delete related scores, else they will have a null beatmap against the model's spec. + foreach (var score in beatmap.Scores) + realm.Remove(score); - realm.Remove(s); + realm.Remove(beatmap); + } + + realm.Remove(beatmapSet); } var pendingDeleteSkins = realm.All().Where(s => s.DeletePending); From 34aa1bf21d46e404fb18e0e5ca6045967c2a7f2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 15:34:39 +0900 Subject: [PATCH 157/227] Sanitise and remove some usages of `Detach` which are no longer required --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 3 +-- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 1 - osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +--- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 22b792cee4..9b583494cb 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -10,7 +10,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; @@ -162,7 +161,7 @@ namespace osu.Game.Tests.Scores.IO var scoreManager = osu.Dependencies.Get(); await scoreManager.Import(score, archive); - return scoreManager.Query(_ => true).Detach(); + return scoreManager.Query(_ => true); } internal class TestArchiveReader : ArchiveReader diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 17168bf07f..63df6a6390 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.UserInterface imported?.PerformRead(s => { - beatmapInfo = s.Beatmaps[0].Detach(); + beatmapInfo = s.Beatmaps[0]; for (int i = 0; i < 50; i++) { diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 8a2ee95020..fb32c11365 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -188,7 +188,7 @@ namespace osu.Game.Beatmaps public int BeatmapVersion; - public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone(); + public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone(); public override string ToString() => this.GetDisplayTitle(); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index e1399c5272..6947752c47 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -84,7 +84,6 @@ namespace osu.Game.Beatmaps if (working != null) return working; - // TODO: FUCK THE WORLD :D beatmapInfo = beatmapInfo.Detach(); workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f355554482..8b4de447e5 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -682,9 +682,7 @@ namespace osu.Game.Screens.Select if (beatmapSet.Beatmaps.All(b => b.Hidden)) return null; - // TODO: FUCK THE WORLD :D - if (beatmapSet.IsManaged) - beatmapSet = beatmapSet.Detach(); + beatmapSet = beatmapSet.Detach(); var set = new CarouselBeatmapSet(beatmapSet) { From e12025dd48018cbdf4feb298026539b4878f2591 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 16:17:11 +0900 Subject: [PATCH 158/227] Cascade delete metadata when beatmaps are deleted --- osu.Game/Database/RealmContextFactory.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 1aa6b2043c..0802641b06 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -121,6 +121,8 @@ namespace osu.Game.Database foreach (var score in beatmap.Scores) realm.Remove(score); + realm.Remove(beatmap.Metadata); + realm.Remove(beatmap); } From eb70a1eeb70004c71e301ca9163eacd5bd351026 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 18:05:25 +0900 Subject: [PATCH 159/227] Replace compatibility properties with direct references --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Models/RealmNamedFileUsage.cs | 10 ---------- .../Edit/Checks/CheckTooShortAudioFiles.cs | 2 +- .../Rulesets/Edit/Checks/CheckZeroByteFiles.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 14 ++++---------- osu.Game/Scoring/ScoreManager.cs | 8 ++++---- osu.Game/Scoring/ScoreModelManager.cs | 4 ++-- osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 2 +- 10 files changed, 16 insertions(+), 32 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index fb32c11365..1c1f81f143 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -38,7 +38,7 @@ namespace osu.Game.Beatmaps public BeatmapMetadata Metadata { get; set; } = new BeatmapMetadata(); [IgnoreMap] - [Backlink(nameof(ScoreInfo.Beatmap))] + [Backlink(nameof(ScoreInfo.BeatmapInfo))] public IQueryable Scores { get; } = null!; public BeatmapInfo(RulesetInfo ruleset, BeatmapDifficulty difficulty, BeatmapMetadata metadata) diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs index 801c826292..17e32510a8 100644 --- a/osu.Game/Models/RealmNamedFileUsage.cs +++ b/osu.Game/Models/RealmNamedFileUsage.cs @@ -31,15 +31,5 @@ namespace osu.Game.Models } IFileInfo INamedFileUsage.File => File; - - #region Compatibility properties - - public RealmFile FileInfo - { - get => File; - set => File = value; - } - - #endregion } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs index 5cc98c5537..381ebd4af6 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Edit.Checks foreach (var file in beatmapSet.Files) { - using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.GetStoragePath())) + using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) { if (data == null) continue; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs index ab9959aec2..324ebe6b50 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Edit.Checks foreach (var file in beatmapSet.Files) { - using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.GetStoragePath())) + using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) { if (data?.Length == 0) yield return new IssueTemplateZeroBytes(this).Create(file.Filename); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 61df736a4a..ffec54aca7 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -49,7 +49,7 @@ namespace osu.Game.Scoring public ScoreInfo(BeatmapInfo beatmap, RulesetInfo ruleset, RealmUser realmUser) { Ruleset = ruleset; - Beatmap = beatmap; + BeatmapInfo = beatmap; RealmUser = realmUser; } @@ -94,7 +94,7 @@ namespace osu.Game.Scoring public double? PP { get; set; } - public BeatmapInfo Beatmap { get; set; } = null!; + public BeatmapInfo BeatmapInfo { get; set; } = null!; public RulesetInfo Ruleset { get; set; } = null!; @@ -129,7 +129,7 @@ namespace osu.Game.Scoring public int RankInt { get; set; } IRulesetInfo IScoreInfo.Ruleset => Ruleset; - IBeatmapInfo IScoreInfo.Beatmap => Beatmap; + IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo; IUser IScoreInfo.User => User; IEnumerable IHasNamedFiles.Files => Files; @@ -139,13 +139,7 @@ namespace osu.Game.Scoring private Mod[]? mods; - public Guid BeatmapInfoID => Beatmap.ID; - - public BeatmapInfo BeatmapInfo - { - get => Beatmap; - set => Beatmap = value; - } + public Guid BeatmapInfoID => BeatmapInfo.ID; public int UserID => RealmUser.OnlineID; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index ce5d2acc21..ad2be7d813 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -70,7 +70,7 @@ namespace osu.Game.Scoring // Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below. foreach (var s in scores) { - await difficultyCache.GetDifficultyAsync(s.Beatmap, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); + await difficultyCache.GetDifficultyAsync(s.BeatmapInfo, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } } @@ -154,11 +154,11 @@ namespace osu.Game.Scoring // This score is guaranteed to be an osu!stable score. // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. - if (score.Beatmap.MaxCombo != null) - beatmapMaxCombo = score.Beatmap.MaxCombo.Value; + if (score.BeatmapInfo.MaxCombo != null) + beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value; else { - if (!score.Beatmap.IsManaged || difficulties == null) + if (!score.BeatmapInfo.IsManaged || difficulties == null) { // We don't have enough information (max combo) to compute the score, so use the provided score. return score.TotalScore; diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 1068aba231..09a5919f12 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -58,8 +58,8 @@ namespace osu.Game.Scoring protected override Task Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { // Ensure the beatmap is not detached. - if (!model.Beatmap.IsManaged) - model.Beatmap = realm.Find(model.Beatmap.ID); + if (!model.BeatmapInfo.IsManaged) + model.BeatmapInfo = realm.Find(model.BeatmapInfo.ID); if (!model.Ruleset.IsManaged) model.Ruleset = realm.Find(model.Ruleset.ShortName); diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 9a0085c62a..fb1fa09253 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Select.Carousel base.LoadComplete(); scoreSubscription = realmFactory.Context.All() - .Filter($"{nameof(ScoreInfo.Beatmap)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) + .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) .QueryAsyncWithNotifications((_, changes, ___) => { if (changes == null) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index d03a8b645d..49f2ea5d64 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Select.Leaderboards return; scoreSubscription = realmFactory.Context.All() - .Filter($"{nameof(ScoreInfo.Beatmap)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) + .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) .QueryAsyncWithNotifications((_, changes, ___) => { if (changes == null) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 9502dd8968..ff496aea36 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -97,7 +97,7 @@ namespace osu.Game.Storyboards { Drawable drawable = null; - string storyboardPath = BeatmapInfo.BeatmapSet?.Files.FirstOrDefault(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.FileInfo.GetStoragePath(); + string storyboardPath = BeatmapInfo.BeatmapSet?.Files.FirstOrDefault(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); if (!string.IsNullOrEmpty(storyboardPath)) drawable = new Sprite { Texture = textureStore.Get(storyboardPath) }; From ef0f794fd61c61ec776380bfdc3a5479f765bf3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 18:08:26 +0900 Subject: [PATCH 160/227] Remove stay newline --- osu.Game/Screens/Select/BeatmapCarousel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 8b4de447e5..734d9bbde7 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -158,7 +158,6 @@ namespace osu.Game.Screens.Select private readonly DrawablePool setPool = new DrawablePool(100); public BeatmapCarousel() - { root = new CarouselRoot(this); InternalChild = new OsuContextMenuContainer From 38cc1ce098c9e957a0d46106c2f8857177148f38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 18:51:30 +0900 Subject: [PATCH 161/227] Add missing ruleset in test scores --- .../Visual/SongSelect/TestSceneUserTopScoreContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs index 7af9e9eb40..dd7f9951bf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs @@ -10,6 +10,7 @@ using osuTK.Graphics; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Users; @@ -61,6 +62,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { Id = 6602580, @@ -79,6 +81,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { Id = 4608074, @@ -97,6 +100,7 @@ namespace osu.Game.Tests.Visual.SongSelect Accuracy = 1, MaxCombo = 244, TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, User = new APIUser { Id = 1541390, From 51251e3204aa4b5097eabecc0f312d244672022f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 22:34:07 +0900 Subject: [PATCH 162/227] Fix CI reported warnings --- .../Statistics/AccuracyHeatmap.cs | 2 +- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- .../Beatmaps/IO/OszArchiveReaderTest.cs | 2 +- .../Beatmaps/TestSceneEditorBeatmap.cs | 68 +++++++++++++++++-- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 6 -- .../TestSceneMultiplayerMatchSongSelect.cs | 2 +- .../SongSelect/TestSceneAdvancedStats.cs | 2 +- osu.Game.Tournament/TournamentGameBase.cs | 2 +- .../Online/API/Requests/Responses/APIScore.cs | 4 +- osu.Game/Online/Rooms/MultiplayerScore.cs | 2 +- .../Sections/Ranks/DrawableProfileScore.cs | 10 ++- .../Edit/Checks/CheckBackgroundQuality.cs | 2 +- .../Rulesets/Edit/Checks/CheckFilePresence.cs | 2 +- .../Edit/Checks/CheckTooShortAudioFiles.cs | 45 ++++++------ .../Edit/Checks/CheckZeroByteFiles.cs | 11 +-- .../Mods/DifficultyAdjustSettingsControl.cs | 3 - .../Components/Menus/DifficultyMenuItem.cs | 2 +- .../Screens/Edit/Compose/ComposeScreen.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 3 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 +- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 2 +- .../Ranking/Statistics/StatisticsPanel.cs | 6 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- osu.Game/Screens/Select/FilterControl.cs | 2 +- .../Tests/Beatmaps/HitObjectSampleTest.cs | 2 + 25 files changed, 123 insertions(+), 65 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index db4a6eb50b..6c76da7925 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Statistics pointGrid.Content = points; - if (score.HitEvents == null || score.HitEvents.Count == 0) + if (score.HitEvents.Count == 0) return; // Todo: This should probably not be done like this. diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index bfd6ff0314..06ed638e0a 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = decodeAsJson(normal); var meta = beatmap.BeatmapInfo.Metadata; - Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineID); + Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet?.OnlineID); Assert.AreEqual("Soleily", meta.Artist); Assert.AreEqual("Soleily", meta.ArtistUnicode); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index b2ab1eeaa6..810ea5dbd0 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Beatmaps.IO var meta = beatmap.Metadata; - Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineID); + Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet?.OnlineID); Assert.AreEqual("Soleily", meta.Artist); Assert.AreEqual("Soleily", meta.ArtistUnicode); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs index bf5b517603..153788c2cf 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; @@ -30,7 +31,13 @@ namespace osu.Game.Tests.Beatmaps AddStep("add beatmap", () => { - Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + }); editorBeatmap.HitObjectAdded += h => addedObject = h; }); @@ -49,7 +56,14 @@ namespace osu.Game.Tests.Beatmaps EditorBeatmap editorBeatmap = null; AddStep("add beatmap", () => { - Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + HitObjects = { hitCircle } + }); editorBeatmap.HitObjectRemoved += h => removedObject = h; }); AddStep("remove hitobject", () => editorBeatmap.Remove(editorBeatmap.HitObjects.First())); @@ -71,7 +85,14 @@ namespace osu.Game.Tests.Beatmaps { EditorBeatmap editorBeatmap; - Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + HitObjects = { hitCircle } + }); editorBeatmap.HitObjectUpdated += h => changedObject = h; }); @@ -91,7 +112,13 @@ namespace osu.Game.Tests.Beatmaps AddStep("add beatmap", () => { - Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + }); editorBeatmap.HitObjectUpdated += h => changedObject = h; }); @@ -111,7 +138,14 @@ namespace osu.Game.Tests.Beatmaps public void TestRemovedHitObjectStartTimeChangeEvent() { var hitCircle = new HitCircle(); - var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + var editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + HitObjects = { hitCircle } + }); HitObject changedObject = null; editorBeatmap.HitObjectUpdated += h => changedObject = h; @@ -131,6 +165,10 @@ namespace osu.Game.Tests.Beatmaps { var editorBeatmap = new EditorBeatmap(new OsuBeatmap { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, HitObjects = { new HitCircle(), @@ -156,6 +194,10 @@ namespace osu.Game.Tests.Beatmaps var editorBeatmap = new EditorBeatmap(new OsuBeatmap { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, HitObjects = { new HitCircle(), @@ -185,7 +227,13 @@ namespace osu.Game.Tests.Beatmaps { updatedObjects.Clear(); - Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + }); for (int i = 0; i < 10; i++) { @@ -220,7 +268,13 @@ namespace osu.Game.Tests.Beatmaps { updatedObjects.Clear(); - Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + }); editorBeatmap.Add(new HitCircle()); }); diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 9b583494cb..2b6c9f76fc 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -9,11 +9,9 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; -using osu.Game.Beatmaps; using osu.Game.IO.Archives; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -151,12 +149,8 @@ namespace osu.Game.Tests.Scores.IO public static async Task LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { - var beatmapManager = osu.Dependencies.Get(); - // clone to avoid attaching the input score to realm. score = score.DeepClone(); - score.BeatmapInfo ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - score.Ruleset ??= new OsuRuleset().RulesetInfo; var scoreManager = osu.Dependencies.Get(); await scoreManager.Import(score, archive); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 7f7c97ac48..c6fb418a5b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer var beatmap = new BeatmapInfo { - Ruleset = rulesets.GetRuleset(i % 4), + Ruleset = rulesets.GetRuleset(i % 4) ?? throw new InvalidOperationException(), OnlineID = beatmapId, Length = length, BPM = bpm, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index e916e8d9ac..bc3e8553f7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo { - Ruleset = rulesets.GetRuleset(3), + Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException(), BaseDifficulty = new BeatmapDifficulty { CircleSize = 5, diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index d08322a3e8..75b929273c 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -81,7 +81,7 @@ namespace osu.Game.Tournament ladder ??= new LadderInfo(); - ladder.Ruleset.Value = RulesetStore.GetRuleset(ladder.Ruleset.Value?.ShortName) + ladder.Ruleset.Value = RulesetStore.GetRuleset(ladder.Ruleset.Value.ShortName) ?? RulesetStore.AvailableRulesets.First(); bool addedInfo = false; diff --git a/osu.Game/Online/API/Requests/Responses/APIScore.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs index 71f277570d..d8f4ba835d 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScore.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs @@ -86,7 +86,7 @@ namespace osu.Game.Online.API.Requests.Responses /// public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null) { - var ruleset = rulesets.GetRuleset(RulesetID); + var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException(); var rulesetInstance = ruleset.CreateInstance(); @@ -99,7 +99,7 @@ namespace osu.Game.Online.API.Requests.Responses { TotalScore = TotalScore, MaxCombo = MaxCombo, - BeatmapInfo = beatmap, + BeatmapInfo = beatmap ?? new BeatmapInfo(), User = User, Accuracy = Accuracy, OnlineID = OnlineID, diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index b90680a925..f1bb57bd9d 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -73,7 +73,7 @@ namespace osu.Game.Online.Rooms TotalScore = TotalScore, MaxCombo = MaxCombo, BeatmapInfo = beatmap, - Ruleset = rulesets.GetRuleset(playlistItem.RulesetID), + Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException(), Statistics = Statistics, User = User, Accuracy = Accuracy, diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 562be0403e..998f5d158e 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -1,6 +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; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; @@ -131,9 +132,14 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Origin = Anchor.CentreRight, Direction = FillDirection.Horizontal, Spacing = new Vector2(2), - Children = Score.Mods.Select(mod => new ModIcon(rulesets.GetRuleset(Score.RulesetID).CreateInstance().CreateModFromAcronym(mod.Acronym)) + Children = Score.Mods.Select(mod => { - Scale = new Vector2(0.35f) + var ruleset = rulesets.GetRuleset(Score.RulesetID) ?? throw new InvalidOperationException(); + + return new ModIcon(ruleset.CreateInstance().CreateModFromAcronym(mod.Acronym)) + { + Scale = new Vector2(0.35f) + }; }).ToList(), } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs index 7ce2ee802e..1f65752fa6 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Edit.Checks else if (texture.Width < low_width || texture.Height < low_height) yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height); - string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile); + string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet?.GetPathForFile(backgroundFile); using (Stream stream = context.WorkingBeatmap.GetStream(storagePath)) { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs index 33bcac1e75..a1605a11d0 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckFilePresence.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Edit.Checks } // If the file is set, also make sure it still exists. - string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(filename); + string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet?.GetPathForFile(filename); if (storagePath != null) yield break; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs index 381ebd4af6..6015d0a1b2 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -30,32 +30,35 @@ namespace osu.Game.Rulesets.Edit.Checks { var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; - foreach (var file in beatmapSet.Files) + if (beatmapSet != null) { - using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) + foreach (var file in beatmapSet.Files) { - if (data == null) - continue; - - var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); - int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); - - if (decodeStream == 0) + using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) { - // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it. - // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check. - if (hasAudioExtension(file.Filename) && probablyHasAudioData(data)) - yield return new IssueTemplateBadFormat(this).Create(file.Filename); + if (data == null) + continue; - continue; + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); + + if (decodeStream == 0) + { + // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it. + // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check. + if (hasAudioExtension(file.Filename) && probablyHasAudioData(data)) + yield return new IssueTemplateBadFormat(this).Create(file.Filename); + + continue; + } + + long length = Bass.ChannelGetLength(decodeStream); + double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000; + + // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users. + if (ms > 0 && ms < ms_threshold) + yield return new IssueTemplateTooShort(this).Create(file.Filename, ms); } - - long length = Bass.ChannelGetLength(decodeStream); - double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000; - - // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users. - if (ms > 0 && ms < ms_threshold) - yield return new IssueTemplateTooShort(this).Create(file.Filename, ms); } } } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs index 324ebe6b50..75cb08002f 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs @@ -21,12 +21,15 @@ namespace osu.Game.Rulesets.Edit.Checks { var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; - foreach (var file in beatmapSet.Files) + if (beatmapSet != null) { - using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) + foreach (var file in beatmapSet.Files) { - if (data?.Length == 0) - yield return new IssueTemplateZeroBytes(this).Create(file.Filename); + using (Stream data = context.WorkingBeatmap.GetStream(file.File.GetStoragePath())) + { + if (data?.Length == 0) + yield return new IssueTemplateZeroBytes(this).Create(file.Filename); + } } } } diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index 67b24d24d0..5d7565e56b 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs @@ -76,9 +76,6 @@ namespace osu.Game.Rulesets.Mods var difficulty = beatmap.Value.BeatmapInfo.BaseDifficulty; - if (difficulty == null) - return; - // generally should always be implemented, else the slider will have a zero default. if (difficultyBindable.ReadCurrentFromDifficulty == null) return; diff --git a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs index 75dc479c25..f17fe4c3ce 100644 --- a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs +++ b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Components.Menus public BeatmapInfo BeatmapInfo { get; } public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action difficultyChangeFunc) - : base(beatmapInfo.DifficultyName ?? "(unnamed)", null) + : base(string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? "(unnamed)" : beatmapInfo.DifficultyName, null) { BeatmapInfo = beatmapInfo; State.Value = selected; diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 9386538a78..2bdf59b21c 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Compose { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - ruleset = parent.Get>().Value.BeatmapInfo.Ruleset?.CreateInstance(); + ruleset = parent.Get>().Value.BeatmapInfo.Ruleset.CreateInstance(); composer = ruleset?.CreateHitObjectComposer(); // make the composer available to the timeline and other components in this screen. diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 383b59dc01..8c4b458534 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -599,7 +599,8 @@ namespace osu.Game.Screens.Edit if (isNewBeatmap) { // confirming exit without save means we should delete the new beatmap completely. - beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet); + if (playableBeatmap.BeatmapInfo.BeatmapSet != null) + beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet); // eagerly clear contents before restoring default beatmap to prevent value change callbacks from firing. ClearInternal(); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 98fad09192..c9449f3259 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Edit if (beatmapSkin is Skin skin) BeatmapSkin = new EditorBeatmapSkin(skin); - beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap); + beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap); foreach (var obj in HitObjects) trackStartTime(obj); diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 8d726f7752..231d977aab 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Setup new DesignSection(), }; - var rulesetSpecificSection = beatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateEditorSetupSection(); + var rulesetSpecificSection = beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateEditorSetupSection(); if (rulesetSpecificSection != null) sectionsEnumerable.Add(rulesetSpecificSection); diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 827e128467..567a2307dd 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.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; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -13,7 +12,6 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; -using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osuTK; @@ -76,7 +74,7 @@ namespace osu.Game.Screens.Ranking.Statistics if (newScore == null) return; - if (newScore.HitEvents == null || newScore.HitEvents.Count == 0) + if (newScore.HitEvents.Count == 0) { content.Add(new FillFlowContainer { @@ -104,7 +102,7 @@ namespace osu.Game.Screens.Ranking.Statistics // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. Task.Run(() => { - playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); }, loadCancellation.Token).ContinueWith(t => Schedule(() => { var rows = new FillFlowContainer diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index b37877b35f..ea531e89c8 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -324,7 +324,7 @@ namespace osu.Game.Screens.Select }); // no difficulty means it can't have a status to show - if (beatmapInfo.DifficultyName == null) + if (string.IsNullOrEmpty(beatmapInfo.DifficultyName)) StatusPill.Hide(); addInfoLabels(); diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index e54b25873b..b53d64260a 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select if (!maximumStars.IsDefault) criteria.UserStarDifficulty.Max = maximumStars.Value; - criteria.RulesetCriteria = ruleset.Value.CreateInstance()?.CreateRulesetFilterCriteria(); + criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria(); FilterQueryParser.ApplyQueries(criteria, query); return criteria; diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 4e955975a0..10cb210f4d 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -98,6 +98,8 @@ namespace osu.Game.Tests.Beatmaps userSkinInfo.Files.Clear(); userSkinInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = userFile }, userFile)); + Debug.Assert(beatmapInfo.BeatmapSet != null); + beatmapInfo.BeatmapSet.Files.Clear(); beatmapInfo.BeatmapSet.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = beatmapFile }, beatmapFile)); From b5f670cc5b2649b4b0dbecd2098588ca092444c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 23:17:35 +0900 Subject: [PATCH 163/227] Add far too many fixes for ruleset non-nullable requirements --- .../Editor/CatchSelectionBlueprintTestScene.cs | 8 +++++++- .../Editor/TestSceneManiaBeatSnapGrid.cs | 8 +++++++- .../Editor/TestSceneManiaComposeScreen.cs | 4 ++-- .../Editor/TestSceneManiaHitObjectComposer.cs | 4 ++-- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 8 +++++++- .../Editor/TestSceneTaikoHitObjectComposer.cs | 4 ++-- osu.Game.Tests/Editing/EditorChangeHandlerTest.cs | 11 +++++++++-- .../TestSceneHitObjectComposerDistanceSnapping.cs | 8 +++++++- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 11 +++++++++++ .../Visual/Editing/TestSceneDesignSection.cs | 9 ++++++++- .../Visual/Editing/TestSceneDistanceSnapGrid.cs | 9 ++++++++- .../Visual/Editing/TestSceneHitObjectComposer.cs | 9 ++++++++- .../Visual/Editing/TestSceneMetadataSection.cs | 9 ++++++++- osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs | 8 +++++++- osu.Game.Tournament/TournamentGameBase.cs | 5 +++-- 15 files changed, 96 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index d4c2c0f0af..e345e03c96 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -29,7 +29,13 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor protected CatchSelectionBlueprintTestScene() { - EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } }; + EditorBeatmap = new EditorBeatmap(new CatchBeatmap + { + BeatmapInfo = + { + Ruleset = new CatchRuleset().RulesetInfo, + } + }) { Difficulty = { CircleSize = 0 } }; EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 100 diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 5ccb191a9b..50be13c4e0 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -29,7 +29,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo(); [Cached(typeof(EditorBeatmap))] - private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition())); + private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition()) + { + BeatmapInfo = + { + Ruleset = new ManiaRuleset().RulesetInfo + } + }); private readonly ManiaBeatSnapGrid beatSnapGrid; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index a30e09cd29..5dd7c23ab6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { AddStep("setup compose screen", () => { - var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })) + var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }) { BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, - }; + }); Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 01d80881fa..9788dfe844 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -203,10 +203,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { InternalChildren = new Drawable[] { - EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })) + EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }) { BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo } - }, + }), Composer = new ManiaHitObjectComposer(new ManiaRuleset()) }; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index ef43c3a696..c770e2d96f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -40,7 +40,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public TestSceneOsuDistanceSnapGrid() { - editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + } + }); } [SetUp] diff --git a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index 626537053a..55eb2fa66b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -40,10 +40,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor { InternalChildren = new Drawable[] { - EditorBeatmap = new EditorBeatmap(new TaikoBeatmap()) + EditorBeatmap = new EditorBeatmap(new TaikoBeatmap { BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo } - }, + }), new TaikoHitObjectComposer(new TaikoRuleset()) }; diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index 481cb3230e..2d61948a2a 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -2,7 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; @@ -158,7 +159,13 @@ namespace osu.Game.Tests.Editing private (EditorChangeHandler, EditorBeatmap) createChangeHandler() { - var beatmap = new EditorBeatmap(new Beatmap()); + var beatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + }); var changeHandler = new EditorChangeHandler(beatmap); diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 8eb9452736..43f22e4e90 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -35,7 +35,13 @@ namespace osu.Game.Tests.Editing RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - editorBeatmap = new EditorBeatmap(new OsuBeatmap()), + editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + }), Content = new Container { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 2b6c9f76fc..0e6d560167 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -9,9 +9,11 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; +using osu.Game.Beatmaps; using osu.Game.IO.Archives; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -39,6 +41,8 @@ namespace osu.Game.Tests.Scores.IO User = new APIUser { Username = "Test user" }, Date = DateTimeOffset.Now, OnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = new BeatmapInfo() }; var imported = await LoadScoreIntoOsu(osu, toImport); @@ -70,6 +74,8 @@ namespace osu.Game.Tests.Scores.IO var toImport = new ScoreInfo { User = new APIUser { Username = "Test user" }, + BeatmapInfo = new BeatmapInfo(), + Ruleset = new OsuRuleset().RulesetInfo, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; @@ -97,6 +103,8 @@ namespace osu.Game.Tests.Scores.IO var toImport = new ScoreInfo { User = new APIUser { Username = "Test user" }, + BeatmapInfo = new BeatmapInfo(), + Ruleset = new OsuRuleset().RulesetInfo, Statistics = new Dictionary { { HitResult.Perfect, 100 }, @@ -128,6 +136,8 @@ namespace osu.Game.Tests.Scores.IO await LoadScoreIntoOsu(osu, new ScoreInfo { User = new APIUser { Username = "Test user" }, + BeatmapInfo = new BeatmapInfo(), + Ruleset = new OsuRuleset().RulesetInfo, OnlineID = 2 }, new TestArchiveReader()); @@ -137,6 +147,7 @@ namespace osu.Game.Tests.Scores.IO Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { User = new APIUser { Username = "Test user" }, + BeatmapInfo = new BeatmapInfo(), OnlineID = 2 })); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs index 00f2979691..10917df075 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDesignSection.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; using osuTK.Input; @@ -25,7 +26,13 @@ namespace osu.Game.Tests.Visual.Editing [SetUpSteps] public void SetUp() { - AddStep("create blank beatmap", () => editorBeatmap = new EditorBeatmap(new Beatmap())); + AddStep("create blank beatmap", () => editorBeatmap = new EditorBeatmap(new Beatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + } + })); AddStep("create section", () => Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index d1efd22d6f..0d9e06e471 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; @@ -29,7 +30,13 @@ namespace osu.Game.Tests.Visual.Editing public TestSceneDistanceSnapGrid() { - editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + } + }); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index eee0d6672c..145d738f60 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -39,9 +39,16 @@ namespace osu.Game.Tests.Visual.Editing { Beatmap.Value = CreateWorkingBeatmap(new Beatmap { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + }, HitObjects = new List { - new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f }, + new HitCircle + { + Position = new Vector2(256, 192), Scale = 0.5f + }, new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }, new Slider { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs index 4621436cc6..4ecfb0975b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; @@ -13,7 +14,13 @@ namespace osu.Game.Tests.Visual.Editing public class TestSceneMetadataSection : OsuTestScene { [Cached] - private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap()); + private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + }, + }); private TestMetadataSection metadataSection; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs index 03e78ce854..2f6cf46b21 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs @@ -29,7 +29,13 @@ namespace osu.Game.Tests.Visual.Editing public TestSceneSetupScreen() { - editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + } + }); } [Test] diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 75b929273c..f318c8bd85 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -81,8 +81,9 @@ namespace osu.Game.Tournament ladder ??= new LadderInfo(); - ladder.Ruleset.Value = RulesetStore.GetRuleset(ladder.Ruleset.Value.ShortName) - ?? RulesetStore.AvailableRulesets.First(); + ladder.Ruleset.Value = ladder.Ruleset.Value != null + ? RulesetStore.GetRuleset(ladder.Ruleset.Value.ShortName) + : RulesetStore.AvailableRulesets.First(); bool addedInfo = false; From b2d09b7b10a577602c49f84f47eff3a5a5912a17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Jan 2022 23:42:12 +0900 Subject: [PATCH 164/227] Fix further warnings --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 2 +- .../Visual/Playlists/TestScenePlaylistsRoomCreation.cs | 2 ++ osu.Game/Screens/Select/BeatmapClearScoresDialog.cs | 2 +- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 677aaf6f78..6ec14e6351 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(string.Empty, metadata.Source); Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags); Assert.AreEqual(557821, beatmapInfo.OnlineID); - Assert.AreEqual(241526, beatmapInfo.BeatmapSet.OnlineID); + Assert.AreEqual(241526, beatmapInfo.BeatmapSet?.OnlineID); } } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index eacc3d5e31..2180f0f717 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -145,6 +145,8 @@ namespace osu.Game.Tests.Visual.Playlists modifiedBeatmap.HitObjects.Clear(); modifiedBeatmap.HitObjects.Add(new HitCircle { StartTime = 5000 }); + Debug.Assert(modifiedBeatmap.BeatmapInfo.BeatmapSet != null); + manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet).WaitSafely(); }); diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index afae858b46..774d3b4b28 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Select public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion) { - BodyText = $@"{beatmapInfo.Metadata?.Artist} - {beatmapInfo.Metadata?.Title}"; + BodyText = beatmapInfo.GetDisplayTitle(); Icon = FontAwesome.Solid.Eraser; HeaderText = @"Clearing all local scores. Are you sure?"; Buttons = new PopupDialogButton[] diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index d44d3dce49..f80a980351 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -77,6 +77,6 @@ namespace osu.Game.Skinning } private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) => - new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata?.Author.Username ?? string.Empty }; + new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata.Author.Username ?? string.Empty }; } } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index ff496aea36..b86deeab89 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -78,7 +78,7 @@ namespace osu.Game.Storyboards { get { - string backgroundPath = BeatmapInfo.BeatmapSet?.Metadata?.BackgroundFile; + string backgroundPath = BeatmapInfo.BeatmapSet?.Metadata.BackgroundFile; if (string.IsNullOrEmpty(backgroundPath)) return false; From 4c79145c11fc54f28ec9ef23f12520791f44df43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 00:28:16 +0900 Subject: [PATCH 165/227] Fix potential mod nullref in `APIUserScoreAggregate`'s `CreateScoreInfo` implementation --- .../Online/API/Requests/Responses/APIUserScoreAggregate.cs | 5 ++++- osu.Game/Scoring/ScoreInfo.cs | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs index 9a7f0832a6..a298a8625a 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs @@ -1,7 +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; using Newtonsoft.Json; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; namespace osu.Game.Online.API.Requests.Responses @@ -42,7 +44,8 @@ namespace osu.Game.Online.API.Requests.Responses PP = PP, TotalScore = TotalScore, User = User, - Position = Position + Position = Position, + Mods = Array.Empty() }; } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index ffec54aca7..45e5b8f1a7 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -186,14 +186,12 @@ namespace osu.Game.Scoring { get { - var rulesetInstance = Ruleset.CreateInstance(); - Mod[] scoreMods = Array.Empty(); if (mods != null) scoreMods = mods; else if (localAPIMods != null) - scoreMods = APIMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + scoreMods = APIMods.Select(m => m.ToMod(Ruleset.CreateInstance())).ToArray(); return scoreMods; } From ebe8ba3c3c16289e5d824278fda1a7f110fda2b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 12:47:11 +0900 Subject: [PATCH 166/227] Fix unintended change to dotsetting --- osu.sln.DotSettings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 0da109ae7c..44d75f265c 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -789,7 +789,7 @@ See the LICENCE file in the repository root for full licence text. <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - True + True True True True From 1a29098f3b99a804c6c51cc8067455dfaf3c0959 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 12:48:45 +0900 Subject: [PATCH 167/227] Change default value and add comment explaining why skins are never "locally available" --- osu.Game/Skinning/SkinModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index 2aa21c0629..dbd209e984 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -263,6 +263,6 @@ namespace osu.Game.Skinning }); } - public override bool IsAvailableLocally(SkinInfo model) => false; + public override bool IsAvailableLocally(SkinInfo model) => true; // skins do not have online download support yet. } } From 65dd80e6f63cb7523c687030d2f65b2084011c34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 12:59:16 +0900 Subject: [PATCH 168/227] Sanitise mods / statistics cache logic in `ScoreInfo` --- osu.Game/Scoring/ScoreInfo.cs | 101 +++++++++++++++++----------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 45e5b8f1a7..a10039bc72 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -46,6 +46,12 @@ namespace osu.Game.Scoring [MapTo("User")] public RealmUser RealmUser { get; set; } = new RealmUser(); + [MapTo("Mods")] + public string ModsJson { get; set; } = string.Empty; + + [MapTo("Statistics")] + public string StatisticsJson { get; set; } = string.Empty; + public ScoreInfo(BeatmapInfo beatmap, RulesetInfo ruleset, RealmUser realmUser) { Ruleset = ruleset; @@ -98,27 +104,6 @@ namespace osu.Game.Scoring public RulesetInfo Ruleset { get; set; } = null!; - private Dictionary? statistics; - - [Ignored] - public Dictionary Statistics - { - get - { - if (statistics != null) - return statistics; - - if (!string.IsNullOrEmpty(StatisticsJson)) - statistics = JsonConvert.DeserializeObject>(StatisticsJson); - - return statistics ??= new Dictionary(); - } - set => statistics = value; - } - - [MapTo("Statistics")] - public string StatisticsJson { get; set; } = null!; - public ScoreRank Rank { get => (ScoreRank)RankInt; @@ -135,10 +120,6 @@ namespace osu.Game.Scoring #region Properties required to make things work with existing usages - private APIMod[]? localAPIMods; - - private Mod[]? mods; - public Guid BeatmapInfoID => BeatmapInfo.ID; public int UserID => RealmUser.OnlineID; @@ -177,61 +158,83 @@ namespace osu.Game.Scoring [Ignored] public bool IsLegacyScore => Mods.OfType().Any(); - // Used for database serialisation/deserialisation. - [MapTo("Mods")] - public string ModsJson { get; set; } = string.Empty; + private Dictionary? statistics; + + [Ignored] + public Dictionary Statistics + { + get + { + if (statistics != null) + return statistics; + + if (!string.IsNullOrEmpty(StatisticsJson)) + statistics = JsonConvert.DeserializeObject>(StatisticsJson); + + return statistics ??= new Dictionary(); + } + set => statistics = value; + } + + private Mod[]? mods; [Ignored] public Mod[] Mods { get { - Mod[] scoreMods = Array.Empty(); - if (mods != null) - scoreMods = mods; - else if (localAPIMods != null) - scoreMods = APIMods.Select(m => m.ToMod(Ruleset.CreateInstance())).ToArray(); + return mods; - return scoreMods; + if (apiMods != null) + return APIMods.Select(m => m.ToMod(Ruleset.CreateInstance())).ToArray(); + + return Array.Empty(); } set { - localAPIMods = null; + apiMods = null; mods = value; - ModsJson = JsonConvert.SerializeObject(APIMods); + + updateModsJson(); } } + private APIMod[]? apiMods; + // Used for API serialisation/deserialisation. [Ignored] public APIMod[] APIMods { get { - if (localAPIMods == null) - { - // prioritise reading from realm backing - if (!string.IsNullOrEmpty(ModsJson)) - localAPIMods = JsonConvert.DeserializeObject(ModsJson); + if (apiMods != null) return apiMods; - // then check mods set via Mods property. - if (mods != null) - localAPIMods = mods.Select(m => new APIMod(m)).ToArray(); - } + // prioritise reading from realm backing + if (!string.IsNullOrEmpty(ModsJson)) + apiMods = JsonConvert.DeserializeObject(ModsJson); - return localAPIMods ?? Array.Empty(); + // then check mods set via Mods property. + if (mods != null) + apiMods = mods.Select(m => new APIMod(m)).ToArray(); + + return apiMods ?? Array.Empty(); } set { - localAPIMods = value; + apiMods = value; + mods = null; // We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary. - mods = null; - ModsJson = JsonConvert.SerializeObject(APIMods); + updateModsJson(); } } + private void updateModsJson() + { + ModsJson = JsonConvert.SerializeObject(APIMods); + } + public IEnumerable GetStatisticsForDisplay() { foreach (var r in Ruleset.CreateInstance().GetHitResults()) From 085893c9b41992b1a8ea74c77d58faf43b9d2d74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 13:03:57 +0900 Subject: [PATCH 169/227] Fix stray bracket --- osu.Game/Database/RealmObjectExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 188d619627..a162bd64d3 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -154,8 +154,7 @@ namespace osu.Game.Database return realmList.Select(l => new RealmLiveUnmanaged(l)).Cast>().ToList(); } - public static ILive ToLiveUnmanaged(this T realmObject - ) + public static ILive ToLiveUnmanaged(this T realmObject) where T : RealmObject, IHasGuidPrimaryKey { return new RealmLiveUnmanaged(realmObject); From 7baff18764322ddc8c674b0ab29105c996a24f7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 13:14:44 +0900 Subject: [PATCH 170/227] Add back `PerformRead` return safety by checking `IsManaged` status of returned data --- osu.Game/Database/RealmLive.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 45bc0e4200..6594224666 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -61,15 +61,18 @@ namespace osu.Game.Database /// The action to perform. public TReturn PerformRead(Func perform) { - // TODO: this is weird and kinda wrong... unmanaged objects should be allowed? - // if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) - // throw new InvalidOperationException(@$"Realm live objects should not exit the scope of {nameof(PerformRead)}."); - if (!IsManaged) return perform(data); using (var realm = realmFactory.CreateContext()) - return perform(realm.Find(ID)); + { + var returnData = perform(realm.Find(ID)); + + if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) + throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); + + return returnData; + } } /// From 86b2ac3217dde65e3f9ef6b9fb46c3e5be3f2ece Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 13:19:49 +0900 Subject: [PATCH 171/227] Remove unnecessary `Ruleset` null check in `BeatmapDifficultyCache` --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 981120c5a8..f102daeef5 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -136,7 +136,7 @@ namespace osu.Game.Beatmaps var localRulesetInfo = rulesetInfo as RulesetInfo; // Difficulty can only be computed if the beatmap and ruleset are locally available. - if (localBeatmapInfo?.Ruleset == null || localRulesetInfo == null) + if (localBeatmapInfo == null || localRulesetInfo == null) { // If not, fall back to the existing star difficulty (e.g. from an online source). return Task.FromResult(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0)); From b77cb344d56ee1d92eee8b6d9fefc269ad7b165a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 13:23:41 +0900 Subject: [PATCH 172/227] Use `ctor` rather than `MemberwiseClone` to guarantee a safer clone of `BeatmapDifficulty` --- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs | 2 ++ osu.Game/Beatmaps/BeatmapDifficulty.cs | 7 +------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 61f932fd98..613874b7d6 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -208,6 +208,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps #region Overrides of BeatmapDifficulty + public override BeatmapDifficulty Clone() => new TaikoMultiplierAppliedDifficulty(this); + public override void CopyTo(BeatmapDifficulty other) { base.CopyTo(other); diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index f6e8b8c57d..7e0462f1e8 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -37,12 +37,7 @@ namespace osu.Game.Beatmaps /// /// Returns a shallow-clone of this . /// - public BeatmapDifficulty Clone() - { - var diff = (BeatmapDifficulty)MemberwiseClone(); - CopyTo(diff); - return diff; - } + public virtual BeatmapDifficulty Clone() => new BeatmapDifficulty(this); public virtual void CopyTo(BeatmapDifficulty difficulty) { From 7a81fe19f6ba8ae6f266924cb8e8046f9aa0659c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 13:28:46 +0900 Subject: [PATCH 173/227] Bump realm schema version to allow upgrades --- osu.Game/Database/RealmContextFactory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0802641b06..5cdae15935 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -47,8 +47,9 @@ namespace osu.Game.Database /// 10 2021-11-22 Use ShortName instead of RulesetID for ruleset settings. /// 11 2021-11-22 Use ShortName instead of RulesetID for ruleset key bindings. /// 12 2021-11-24 Add Status to RealmBeatmapSet. + /// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields). /// - private const int schema_version = 12; + private const int schema_version = 13; /// /// Lock object which is held during sections, blocking context creation during blocking periods. From dcc354aa7c24640c8aceef89623c0efce472b273 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 13:40:09 +0900 Subject: [PATCH 174/227] Fix deleted scores not being cleaned up on next startup --- osu.Game/Database/RealmContextFactory.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 5cdae15935..9307e06be0 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -18,6 +18,7 @@ using osu.Game.Models; using osu.Game.Skinning; using osu.Game.Stores; using osu.Game.Rulesets; +using osu.Game.Scoring; using Realms; #nullable enable @@ -112,6 +113,11 @@ namespace osu.Game.Database using (var realm = CreateContext()) using (var transaction = realm.BeginWrite()) { + var pendingDeleteScores = realm.All().Where(s => s.DeletePending); + + foreach (var score in pendingDeleteScores) + realm.Remove(score); + var pendingDeleteSets = realm.All().Where(s => s.DeletePending); foreach (var beatmapSet in pendingDeleteSets) From 0a133c7e97c5184f2e8fe3c6edfc70bd254508b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 13:47:23 +0900 Subject: [PATCH 175/227] Fix typo in comment in `IntroScreen` --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index c4343625ab..0f5dbb2842 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Menu if (!resuming) { // generally this can never be null - // an exception is running ruleset tests, where the osu! ruleset may not be prsent (causing importing the intro to fail). + // an exception is running ruleset tests, where the osu! ruleset may not be present (causing importing the intro to fail). if (initialBeatmap != null) beatmap.Value = initialBeatmap; Track = beatmap.Value.Track; From 6025fe325df0d49e5526b697d514d86c0788259a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 15:08:51 +0900 Subject: [PATCH 176/227] Fix filter criteria not being applied after carousel loads new beatmap sets --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 734d9bbde7..13ece08144 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -124,8 +124,7 @@ namespace osu.Game.Screens.Select itemsCache.Invalidate(); ScrollToSelected(); - // apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false). - FlushPendingFilterOperations(); + applyActiveCriteria(false); // Run on late scheduler want to ensure this runs after all pending UpdateBeatmapSet / RemoveBeatmapSet operations are run. SchedulerAfterChildren.Add(() => From 88145dedf14bf0cedf13aabf6d7d1be29a050ac2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 15:27:43 +0900 Subject: [PATCH 177/227] Remove oudated comments --- osu.Game/Stores/RealmArchiveModelImporter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 4aca079e2e..9927840c4e 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -393,9 +393,6 @@ namespace osu.Game.Stores LogForModel(item, @"Found existing but failed re-use check."); existing.DeletePending = true; - - // todo: actually delete? i don't think this is required... - // ModelStore.PurgeDeletable(s => s.ID == existing.ID); } PreImport(item, realm); From 70c107b434eeab82d57f71e34556347d59a7aff3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 16:26:35 +0900 Subject: [PATCH 178/227] Remove pointless override method in `RealmArchiveModelManager` --- osu.Game/Stores/RealmArchiveModelManager.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 4365fdab1e..b456dae343 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -5,12 +5,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Overlays.Notifications; using Realms; @@ -93,11 +90,6 @@ namespace osu.Game.Stores item.Files.Add(namedUsage); } - public override async Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) - { - return await base.Import(item, archive, lowPriority, cancellationToken).ConfigureAwait(false); - } - /// /// Delete multiple items. /// This will post notifications tracking progress. From bdb2979b2e895e0e85ef2137114f44ccf270ea48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 16:27:07 +0900 Subject: [PATCH 179/227] Remove `async` from `Populate` method --- osu.Game/Scoring/ScoreModelManager.cs | 5 +---- osu.Game/Skinning/SkinModelManager.cs | 5 +---- osu.Game/Stores/BeatmapImporter.cs | 5 +---- osu.Game/Stores/RealmArchiveModelImporter.cs | 12 ++++++------ 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 09a5919f12..fcf7244226 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Logging; using osu.Framework.Platform; @@ -55,7 +54,7 @@ namespace osu.Game.Scoring public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); - protected override Task Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { // Ensure the beatmap is not detached. if (!model.BeatmapInfo.IsManaged) @@ -66,8 +65,6 @@ namespace osu.Game.Scoring if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); - - return Task.CompletedTask; } public override bool IsAvailableLocally(ScoreInfo model) diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index dbd209e984..b8313f63a3 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Logging; using osu.Framework.Platform; @@ -49,7 +48,7 @@ namespace osu.Game.Skinning protected override bool HasCustomHashFunction => true; - protected override Task Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + protected override void Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { var skinInfoFile = model.Files.SingleOrDefault(f => f.Filename == skin_info_file); @@ -83,8 +82,6 @@ namespace osu.Game.Skinning model.InstantiationInfo = createInstance(model).GetType().GetInvariantInstantiationInfo(); checkSkinIniMetadata(model, realm); - - return Task.CompletedTask; } private void checkSkinIniMetadata(SkinInfo item, Realm realm) diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 429d3951cf..53971b1d96 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Audio.Track; using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; @@ -53,7 +52,7 @@ namespace osu.Game.Stores protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz"; - protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) + protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { if (archive != null) beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm)); @@ -87,8 +86,6 @@ namespace osu.Game.Stores LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); } } - - return Task.CompletedTask; } protected override void PreImport(BeatmapSetInfo beatmapSet, Realm realm) diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 9927840c4e..bcae9f2451 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -318,7 +318,7 @@ namespace osu.Game.Stores /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - public virtual async Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public virtual Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { using (var realm = ContextFactory.CreateContext()) { @@ -352,7 +352,7 @@ namespace osu.Game.Stores transaction.Commit(); } - return existing.ToLive(ContextFactory); + return Task.FromResult((ILive?)existing.ToLive(ContextFactory)); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -373,7 +373,7 @@ namespace osu.Game.Stores item.Hash = ComputeHash(item); // TODO: we may want to run this outside of the transaction. - await Populate(item, archive, realm, cancellationToken).ConfigureAwait(false); + Populate(item, archive, realm, cancellationToken); if (!checkedExisting) existing = CheckForExisting(item, realm); @@ -387,7 +387,7 @@ namespace osu.Game.Stores existing.DeletePending = false; transaction.Commit(); - return existing.ToLive(ContextFactory); + return Task.FromResult((ILive?)existing.ToLive(ContextFactory)); } LogForModel(item, @"Found existing but failed re-use check."); @@ -413,7 +413,7 @@ namespace osu.Game.Stores throw; } - return item.ToLive(ContextFactory); + return Task.FromResult((ILive?)item.ToLive(ContextFactory)); } } @@ -480,7 +480,7 @@ namespace osu.Game.Stores /// The archive to use as a reference for population. May be null. /// The current realm context. /// An optional cancellation token. - protected abstract Task Populate(TModel model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default); + protected abstract void Populate(TModel model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default); /// /// Perform any final actions before the import to database executes. From 93c78253d65065ba649f382183d4e12b643ab44a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 16:13:30 +0900 Subject: [PATCH 180/227] Add synchronous fetch flow to `BeatmapOnlineLookupQueue` The async flow doesn't work great with the realm import process. We might be able to improve on this going forward, but for the time being adding a synchronous path seems safest. After all, we are already an an asynchronous (dedicated) thread pool at this point. --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 21 ++++++++++++------- osu.Game/Stores/BeatmapImporter.cs | 6 +----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 444bf09487..aecbd6f6e2 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -54,6 +54,12 @@ namespace osu.Game.Beatmaps prepareLocalCache(); } + public void Update(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) + { + foreach (var b in beatmapSet.Beatmaps) + lookup(beatmapSet, b); + } + public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) { return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); @@ -73,13 +79,17 @@ namespace osu.Game.Beatmaps var req = new GetBeatmapRequest(beatmapInfo); - req.Failure += fail; - try { // intentionally blocking to limit web request concurrency api.Perform(req); + if (req.CompletionState == APIRequestCompletionState.Failed) + { + logForModel(set, $"Online retrieval failed for {beatmapInfo}"); + beatmapInfo.OnlineID = -1; + } + var res = req.Response; if (res != null) @@ -99,13 +109,8 @@ namespace osu.Game.Beatmaps } catch (Exception e) { - fail(e); - } - - void fail(Exception e) - { - beatmapInfo.OnlineID = -1; logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})"); + beatmapInfo.OnlineID = -1; } } diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 53971b1d96..a9cb46ca29 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -71,11 +71,7 @@ namespace osu.Game.Stores bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0); - if (onlineLookupQueue != null) - { - // TODO: this required `BeatmapOnlineLookupQueue` to somehow support new types. - // await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); - } + onlineLookupQueue?.Update(beatmapSet, cancellationToken); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0)) From c61419dfe5606b2749381b9a7eae275d4762e4bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 16:56:09 +0900 Subject: [PATCH 181/227] Fix scores not using correct filename/display strings I've updated all cases where we should have been using `GetDisplayString()` anyway, but left the `ToString()` implementations in place for safety. They should probably be removed in the future. --- osu.Game/Database/LegacyExporter.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 2 ++ osu.Game/Stores/RealmArchiveModelImporter.cs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs index 802ccec6ed..ee960b6b30 100644 --- a/osu.Game/Database/LegacyExporter.cs +++ b/osu.Game/Database/LegacyExporter.cs @@ -35,7 +35,7 @@ namespace osu.Game.Database /// The item to export. public void Export(TModel item) { - string filename = $"{item.ToString().GetValidArchiveContentFilename()}{FileExtension}"; + string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}"; using (var stream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create)) ExportModelTo(item, stream); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index a10039bc72..9b4f4bf784 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -274,5 +274,7 @@ namespace osu.Game.Scoring } #endregion + + public override string ToString() => this.GetDisplayTitle(); } } diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index bcae9f2451..2ea7aecc94 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -169,7 +169,7 @@ namespace osu.Game.Stores else { notification.CompletionText = imported.Count == 1 - ? $"Imported {imported.First()}!" + ? $"Imported {imported.First().GetDisplayString()}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; if (imported.Count > 0 && PostImport != null) From 069d6d2954a437080b2fb0f456ad7a033b296253 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 18:02:02 +0900 Subject: [PATCH 182/227] Remove pointless compatibility parameter `BeatmapSetInfoID` --- osu.Game/Beatmaps/BeatmapInfo.cs | 3 --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 1c1f81f143..078a34e50d 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -163,9 +163,6 @@ namespace osu.Game.Beatmaps } } - [Ignored] - public Guid BeatmapSetInfoID => BeatmapSet?.ID ?? Guid.Empty; - [Ignored] [IgnoreMap] public BeatmapDifficulty BaseDifficulty diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 41700bec17..b390cd225a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -486,7 +486,7 @@ namespace osu.Game.Screens.Select { if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50) { - if (beatmap.BeatmapSetInfoID == beatmapInfoPrevious.BeatmapSetInfoID) + if (beatmap.BeatmapSet?.ID == beatmapInfoPrevious.BeatmapSet?.ID) sampleChangeDifficulty.Play(); else sampleChangeBeatmap.Play(); From ded1d87739543fa8370c10a94cd63dbcb09ecb79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 18:23:10 +0900 Subject: [PATCH 183/227] Move `RulesetStore` construction earlier in process so rulesets are available for EF migration --- osu.Game/OsuGameBase.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a003c6b8ab..a9538c1e8c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -189,6 +189,10 @@ namespace osu.Game : null; dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", efContextFactory)); + + dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); + dependencies.CacheAs(RulesetStore); + if (efContextFactory != null) new EFToRealmMigrator(efContextFactory, realmFactory, LocalConfig).Run(); @@ -219,9 +223,6 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); - dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); - dependencies.CacheAs(RulesetStore); - // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realmFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realmFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); From 45a23e5a43f90e897a4761774147540772c7d430 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 17:51:23 +0900 Subject: [PATCH 184/227] Add EF to realm score migration --- osu.Game/Database/EFToRealmMigrator.cs | 91 +++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index b79a982460..1aa364ff8f 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -3,9 +3,13 @@ using System.Linq; using Microsoft.EntityFrameworkCore; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Models; +using osu.Game.Rulesets; +using osu.Game.Scoring; using osu.Game.Skinning; +using Realms; #nullable enable @@ -30,6 +34,70 @@ namespace osu.Game.Database { migrateSettings(db); migrateSkins(db); + + migrateScores(db); + } + } + + private void migrateScores(DatabaseWriteUsage db) + { + // can be removed 20220730. + var existingScores = db.Context.ScoreInfo + .Include(s => s.Ruleset) + .Include(s => s.BeatmapInfo) + .Include(s => s.Files) + .ThenInclude(f => f.FileInfo) + .ToList(); + + // previous entries in EF are removed post migration. + if (!existingScores.Any()) + return; + + using (var realm = realmContextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + // only migrate data if the realm database is empty. + // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. + if (!realm.All().Any()) + { + foreach (var score in existingScores) + { + var realmScore = new ScoreInfo + { + Hash = score.Hash, + DeletePending = score.DeletePending, + OnlineID = score.OnlineID ?? -1, + ModsJson = score.ModsJson, + StatisticsJson = score.StatisticsJson, + User = score.User, + TotalScore = score.TotalScore, + MaxCombo = score.MaxCombo, + Accuracy = score.Accuracy, + HasReplay = ((IScoreInfo)score).HasReplay, + Date = score.Date, + PP = score.PP, + BeatmapInfo = realm.All().First(b => b.Hash == score.BeatmapInfo.Hash), + Ruleset = realm.Find(score.Ruleset.ShortName), + Rank = score.Rank, + HitEvents = score.HitEvents, + Passed = score.Passed, + Combo = score.Combo, + Position = score.Position, + Statistics = score.Statistics, + Mods = score.Mods, + APIMods = score.APIMods, + }; + + migrateFiles(score, realm, realmScore); + + realm.Add(realmScore); + } + } + + db.Context.RemoveRange(existingScores); + // Intentionally don't clean up the files, so they don't get purged by EF. + + transaction.Commit(); } } @@ -77,15 +145,7 @@ namespace osu.Game.Database InstantiationInfo = skin.InstantiationInfo, }; - foreach (var file in skin.Files) - { - var realmFile = realm.Find(file.FileInfo.Hash); - - if (realmFile == null) - realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash }); - - realmSkin.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename)); - } + migrateFiles(skin, realm, realmSkin); realm.Add(realmSkin); @@ -101,6 +161,19 @@ namespace osu.Game.Database } } + private static void migrateFiles(IHasFiles fileSource, Realm realm, IHasRealmFiles realmObject) where T : INamedFileInfo + { + foreach (var file in fileSource.Files) + { + var realmFile = realm.Find(file.FileInfo.Hash); + + if (realmFile == null) + realm.Add(realmFile = new RealmFile { Hash = file.FileInfo.Hash }); + + realmObject.Files.Add(new RealmNamedFileUsage(realmFile, file.Filename)); + } + } + private void migrateSettings(DatabaseWriteUsage db) { // migrate ruleset settings. can be removed 20220315. From b610d2db124a580f233c94ff450ecef3e2fd6111 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 18:02:08 +0900 Subject: [PATCH 185/227] Add EF to realm beatmap migration --- osu.Game/Beatmaps/EFBeatmapInfo.cs | 25 +++--- osu.Game/Database/EFToRealmMigrator.cs | 103 ++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/EFBeatmapInfo.cs b/osu.Game/Beatmaps/EFBeatmapInfo.cs index 8257d0cf7d..8daeaa7030 100644 --- a/osu.Game/Beatmaps/EFBeatmapInfo.cs +++ b/osu.Game/Beatmaps/EFBeatmapInfo.cs @@ -41,7 +41,7 @@ namespace osu.Game.Beatmaps public BeatmapOnlineStatus Status { get; set; } = BeatmapOnlineStatus.None; [Required] - public EFBeatmapSetInfo BeatmapSet { get; set; } + public EFBeatmapSetInfo BeatmapSetInfo { get; set; } public EFBeatmapMetadata Metadata { get; set; } @@ -85,9 +85,10 @@ namespace osu.Game.Beatmaps public float StackLeniency { get; set; } = 0.7f; public bool SpecialStyle { get; set; } - public int RulesetID { get; set; } + [Column("RulesetID")] + public int RulesetInfoID { get; set; } - public EFRulesetInfo Ruleset { get; set; } + public EFRulesetInfo RulesetInfo { get; set; } public bool LetterboxInBreaks { get; set; } public bool WidescreenStoryboard { get; set; } @@ -145,13 +146,13 @@ namespace osu.Game.Beatmaps public bool Equals(IBeatmapInfo other) => other is EFBeatmapInfo b && Equals(b); - public bool AudioEquals(EFBeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && - BeatmapSet.Hash == other.BeatmapSet.Hash && - (Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile; + public bool AudioEquals(EFBeatmapInfo other) => other != null && BeatmapSetInfo != null && other.BeatmapSetInfo != null && + BeatmapSetInfo.Hash == other.BeatmapSetInfo.Hash && + (Metadata ?? BeatmapSetInfo.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSetInfo.Metadata).AudioFile; - public bool BackgroundEquals(EFBeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && - BeatmapSet.Hash == other.BeatmapSet.Hash && - (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile; + public bool BackgroundEquals(EFBeatmapInfo other) => other != null && BeatmapSetInfo != null && other.BeatmapSetInfo != null && + BeatmapSetInfo.Hash == other.BeatmapSetInfo.Hash && + (Metadata ?? BeatmapSetInfo.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSetInfo.Metadata).BackgroundFile; /// /// Returns a shallow-clone of this . @@ -167,16 +168,16 @@ namespace osu.Game.Beatmaps #region Implementation of IBeatmapInfo [JsonIgnore] - IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new EFBeatmapMetadata(); + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSetInfo?.Metadata ?? new EFBeatmapMetadata(); [JsonIgnore] IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty; [JsonIgnore] - IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSet; + IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSetInfo; [JsonIgnore] - IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; + IRulesetInfo IBeatmapInfo.Ruleset => RulesetInfo; #endregion } diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 1aa364ff8f..02237b0ec0 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -35,10 +35,111 @@ namespace osu.Game.Database migrateSettings(db); migrateSkins(db); + migrateBeatmaps(db); migrateScores(db); } } + private void migrateBeatmaps(DatabaseWriteUsage db) + { + // can be removed 20220730. + var existingBeatmapSets = db.Context.EFBeatmapSetInfo + .Include(s => s.Beatmaps).ThenInclude(b => b.RulesetInfo) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Files).ThenInclude(f => f.FileInfo) + .ToList(); + + // previous entries in EF are removed post migration. + if (!existingBeatmapSets.Any()) + return; + + using (var realm = realmContextFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) + { + // only migrate data if the realm database is empty. + // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. + if (!realm.All().Any(s => !s.Protected)) + { + foreach (var beatmapSet in existingBeatmapSets) + { + var realmBeatmapSet = new BeatmapSetInfo + { + OnlineID = beatmapSet.OnlineID ?? -1, + DateAdded = beatmapSet.DateAdded, + Status = beatmapSet.Status, + DeletePending = beatmapSet.DeletePending, + Hash = beatmapSet.Hash, + Protected = beatmapSet.Protected, + }; + + migrateFiles(beatmapSet, realm, realmBeatmapSet); + + foreach (var beatmap in beatmapSet.Beatmaps) + { + var realmBeatmap = new BeatmapInfo + { + DifficultyName = beatmap.DifficultyName, + Status = beatmap.Status, + OnlineID = beatmap.OnlineID ?? -1, + Length = beatmap.Length, + BPM = beatmap.BPM, + Hash = beatmap.Hash, + StarRating = beatmap.StarRating, + MD5Hash = beatmap.MD5Hash, + Hidden = beatmap.Hidden, + AudioLeadIn = beatmap.AudioLeadIn, + StackLeniency = beatmap.StackLeniency, + SpecialStyle = beatmap.SpecialStyle, + LetterboxInBreaks = beatmap.LetterboxInBreaks, + WidescreenStoryboard = beatmap.WidescreenStoryboard, + EpilepsyWarning = beatmap.EpilepsyWarning, + SamplesMatchPlaybackRate = beatmap.SamplesMatchPlaybackRate, + DistanceSpacing = beatmap.DistanceSpacing, + BeatDivisor = beatmap.BeatDivisor, + GridSize = beatmap.GridSize, + TimelineZoom = beatmap.TimelineZoom, + Countdown = beatmap.Countdown, + CountdownOffset = beatmap.CountdownOffset, + MaxCombo = beatmap.MaxCombo, + Bookmarks = beatmap.Bookmarks, + Ruleset = realm.Find(beatmap.RulesetInfo.ShortName), + Difficulty = new BeatmapDifficulty(beatmap.BaseDifficulty), + Metadata = new BeatmapMetadata + { + Title = beatmap.Metadata.Title, + TitleUnicode = beatmap.Metadata.TitleUnicode, + Artist = beatmap.Metadata.Artist, + ArtistUnicode = beatmap.Metadata.ArtistUnicode, + Author = new RealmUser + { + OnlineID = beatmap.Metadata.Author.Id, + Username = beatmap.Metadata.Author.Username, + }, + Source = beatmap.Metadata.Source, + Tags = beatmap.Metadata.Tags, + PreviewTime = beatmap.Metadata.PreviewTime, + AudioFile = beatmap.Metadata.AudioFile, + BackgroundFile = beatmap.Metadata.BackgroundFile, + AuthorString = beatmap.Metadata.AuthorString, + }, + BeatmapSet = realmBeatmapSet, + }; + + realmBeatmapSet.Beatmaps.Add(realmBeatmap); + } + + realm.Add(realmBeatmapSet); + } + } + + // db.Context.RemoveRange(existingBeatmapSets); + // Intentionally don't clean up the files, so they don't get purged by EF. + + transaction.Commit(); + } + } + private void migrateScores(DatabaseWriteUsage db) { // can be removed 20220730. @@ -94,7 +195,7 @@ namespace osu.Game.Database } } - db.Context.RemoveRange(existingScores); + // db.Context.RemoveRange(existingScores); // Intentionally don't clean up the files, so they don't get purged by EF. transaction.Commit(); From 2840a71dda2f50b5c934b290e8f7ab676a227412 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 18:28:00 +0900 Subject: [PATCH 186/227] Uncomment EF deletion lines in migrations --- osu.Game/Database/EFToRealmMigrator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index 02237b0ec0..ad0f2c5982 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -133,7 +133,7 @@ namespace osu.Game.Database } } - // db.Context.RemoveRange(existingBeatmapSets); + db.Context.RemoveRange(existingBeatmapSets); // Intentionally don't clean up the files, so they don't get purged by EF. transaction.Commit(); @@ -195,7 +195,7 @@ namespace osu.Game.Database } } - // db.Context.RemoveRange(existingScores); + db.Context.RemoveRange(existingScores); // Intentionally don't clean up the files, so they don't get purged by EF. transaction.Commit(); From c33fe7bcc6961ce24ba246e8c133a21864a6fd30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 18:32:59 +0900 Subject: [PATCH 187/227] Remove one more unnecessary `Detach` operation --- osu.Game/OsuGame.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ad045d08d2..0be6a808d8 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -501,8 +501,6 @@ namespace osu.Game return; } - databasedScoreInfo = databasedScoreInfo.Detach(); - var databasedScore = ScoreManager.GetScore(databasedScoreInfo); if (databasedScore.Replay == null) From 54804ebfbdc1f39b30df11da48bb496f5c2766e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Jan 2022 18:38:38 +0900 Subject: [PATCH 188/227] Fix delete/clear scores buttons not working --- osu.Game/Screens/Select/SongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b390cd225a..837f30eb2b 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -807,14 +807,14 @@ namespace osu.Game.Screens.Select private void delete(BeatmapSetInfo beatmap) { - if (beatmap == null || !beatmap.IsManaged) return; + if (beatmap == null) return; dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } private void clearScores(BeatmapInfo beatmapInfo) { - if (beatmapInfo == null || !beatmapInfo.IsManaged) return; + if (beatmapInfo == null) return; dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmapInfo, () => // schedule done here rather than inside the dialog as the dialog may fade out and never callback. From 9b33fbbee52cff0d1af77cba8ac165b11c11d261 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 13:08:20 +0900 Subject: [PATCH 189/227] Ensure detached when performing model `Clone` operations on `BeatmapInfo`/`ScoreInfo` --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 078a34e50d..b9dd59cfe4 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -185,7 +185,7 @@ namespace osu.Game.Beatmaps public int BeatmapVersion; - public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone(); + public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone(); public override string ToString() => this.GetDisplayTitle(); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 9b4f4bf784..b168726283 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -131,7 +131,7 @@ namespace osu.Game.Scoring public ScoreInfo DeepClone() { - var clone = (ScoreInfo)MemberwiseClone(); + var clone = (ScoreInfo)this.Detach().MemberwiseClone(); clone.Statistics = new Dictionary(clone.Statistics); From 8424d86e9afeec9670ffb7dd9203a588ec09ce4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 13:19:00 +0900 Subject: [PATCH 190/227] Remove unused `cancellationToken` parameter in synchronous `BeatmapOnlineLookupQueue` flow --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 2 +- osu.Game/Stores/BeatmapImporter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index aecbd6f6e2..a5d7ce23ff 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps prepareLocalCache(); } - public void Update(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) + public void Update(BeatmapSetInfo beatmapSet) { foreach (var b in beatmapSet.Beatmaps) lookup(beatmapSet, b); diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index a9cb46ca29..3ebdcde296 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -71,7 +71,7 @@ namespace osu.Game.Stores bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0); - onlineLookupQueue?.Update(beatmapSet, cancellationToken); + onlineLookupQueue?.Update(beatmapSet); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0)) From dea2e1fac0215944b3c6b863158dedbe224aa2e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 13:20:51 +0900 Subject: [PATCH 191/227] Return immediately on failed web request in synchronous `BeatmapOnlineLookupQueue` flow --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index a5d7ce23ff..a24b6b315a 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -88,6 +88,7 @@ namespace osu.Game.Beatmaps { logForModel(set, $"Online retrieval failed for {beatmapInfo}"); beatmapInfo.OnlineID = -1; + return; } var res = req.Response; From 916b591b1b6bc7c3e00fa6e4511926ea6eee2259 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 18:03:06 +0900 Subject: [PATCH 192/227] Fix watch replay button not working immediately after playing --- osu.Game/Online/ScoreDownloadTracker.cs | 2 +- osu.Game/Screens/Play/Player.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index 08362448a3..b34586567d 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realmContextFactory.Context.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9fc0d70cc0..ff3fea51cf 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -18,6 +18,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; using osu.Game.Online.API; @@ -1038,17 +1039,17 @@ namespace osu.Game.Screens.Play replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } - // the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import. - var importableScore = score.ScoreInfo.DeepClone(); - // For the time being, online ID responses are not really useful for anything. // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores. // // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint // conflicts across various systems (ie. solo and multiplayer). - importableScore.OnlineID = -1; + score.ScoreInfo.OnlineID = -1; - await scoreManager.Import(importableScore, replayReader).ConfigureAwait(false); + var imported = await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false); + + // detach post-import as we want to keep using the score for display in results. + score.ScoreInfo = imported.PerformRead(s => s.Detach()); } /// From 34dbde6023512d654bc4a4ad823563165650b23f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 18:22:52 +0900 Subject: [PATCH 193/227] Only copy across `Hash` and `ID` so statistics aren't lost --- osu.Game/Screens/Play/Player.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ff3fea51cf..4d3201cd27 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -18,7 +18,6 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; using osu.Game.Online.API; @@ -1039,17 +1038,24 @@ namespace osu.Game.Screens.Play replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } + // the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import. + var importableScore = score.ScoreInfo.DeepClone(); + // For the time being, online ID responses are not really useful for anything. // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores. // // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint // conflicts across various systems (ie. solo and multiplayer). - score.ScoreInfo.OnlineID = -1; + importableScore.OnlineID = -1; - var imported = await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false); + var imported = await scoreManager.Import(importableScore, replayReader).ConfigureAwait(false); - // detach post-import as we want to keep using the score for display in results. - score.ScoreInfo = imported.PerformRead(s => s.Detach()); + imported.PerformRead(s => + { + // because of the clone above, it's required that we copy back the post-import hash/ID to use for availability matching. + score.ScoreInfo.Hash = s.Hash; + score.ScoreInfo.ID = s.ID; + }); } /// From 7acd1b545f748a07703a85eeeba7230c0eb9ded1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 19:55:09 +0900 Subject: [PATCH 194/227] Remove unnecessary `Live` conversion in `IntroScreen` (handled by `GetWorkingBeatmap`) --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 0f5dbb2842..265ec41f96 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Menu if (s.Beatmaps.Count == 0) return; - initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0].ToLive(realmContextFactory)); + initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]); }); } } From 3a95425a9eeb2a14bd95411129baf30880a6ab10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 19:55:49 +0900 Subject: [PATCH 195/227] Remove left-over methods in `MusicController` --- osu.Game/Overlays/MusicController.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 44c2916f12..70f8332295 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -134,14 +134,6 @@ namespace osu.Game.Overlays /// public bool TrackLoaded => CurrentTrack.TrackLoaded; - private void beatmapUpdated(BeatmapSetInfo set) => Schedule(() => - { - beatmapSets.Remove(set); - beatmapSets.Add(set); - }); - - private void beatmapRemoved(BeatmapSetInfo set) => Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID)); - private ScheduledDelegate seekDelegate; public void SeekTo(double position) From a59dcccab7a4134081dfbc7351eab8b015981684 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 19:59:21 +0900 Subject: [PATCH 196/227] Add local `ContextFactory` caching to all remaining test scenes that create local managers --- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 1 + .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 1 + osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 1 + .../Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 1 + .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 2 ++ .../Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs | 1 + .../Visual/Multiplayer/TestSceneMultiplayerQueueList.cs | 1 + .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 2 ++ .../Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs | 2 ++ .../Visual/Multiplayer/TestScenePlaylistsSongSelect.cs | 1 + osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs | 1 + .../Visual/Playlists/TestScenePlaylistsRoomCreation.cs | 1 + osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 1 + osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs | 1 + .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 1 + 15 files changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index d2b0f7324b..18572ac211 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -38,6 +38,7 @@ namespace osu.Game.Tests.Visual.Collections { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 5bad36c3dd..99c867b014 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -45,6 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index d55ca0b578..373b165acc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -63,6 +63,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index c6fb418a5b..181b0c71f2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); beatmaps = new List(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 98b14fbddb..012a2fd960 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -40,6 +40,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 813b62176b..d547b42891 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); } [SetUp] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index b5d31c7ae9..965b142ed7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 0b2ab1aef3..1c346e09d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -42,6 +42,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 6b1001f6b7..221732910b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -43,6 +43,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 08fcac125d..0b0006e437 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -36,6 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 97d17ada71..39cde0ad87 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 2180f0f717..dfcf657218 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Playlists { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); } [SetUpSteps] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 78df4d8d98..a314ed8a6e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -46,6 +46,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(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); + Dependencies.Cache(ContextFactory); return dependencies; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 1ee59eccc7..ca8e9d2eff 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -38,6 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelect { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(ContextFactory); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 63df6a6390..02746ee8e8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -90,6 +90,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(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); + Dependencies.Cache(ContextFactory); var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); From 289ae7c72f18dd22f93925437b44acb5347550d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 21:39:42 +0900 Subject: [PATCH 197/227] Update one more mismatched test implementation --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index a314ed8a6e..9ae15c387a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -45,7 +45,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(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); Dependencies.Cache(ContextFactory); return dependencies; From 9900a3f408b084dedca6cee885bebf7e2ac39073 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 21:40:35 +0900 Subject: [PATCH 198/227] Remove outdated comment --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index d7e3951ac2..0f5bea10e8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -429,7 +429,6 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 20; i++) { - // index + 1 because we are using OnlineID which should never be zero. var set = TestResources.CreateTestBeatmapSetInfo(); // only need to set the first as they are a shared reference. From e558fd69d22fcf17dffce8e4b2d90b5c7042a513 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 23:26:29 +0900 Subject: [PATCH 199/227] Remove unnecessary null check and associated comment --- osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index 5bac2635f5..1034f208a9 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual { base.Update(); - if (Beatmap.Value.TrackLoaded && Beatmap.Value.Track != null) // null check... wasn't required until now? + if (Beatmap.Value.TrackLoaded) { // note that this will override any mod rate application Beatmap.Value.Track.Tempo.Value = Clock.Rate; From 8d4a3cc56959aa0e07425bef9dd7b159a4c58d32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 23:27:33 +0900 Subject: [PATCH 200/227] Remove pointless `Metadata` set --- osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index b6a08dd2ab..c5f56cae9e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestEmptyLegacyBeatmapSkinFallsBack() { - CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo { Metadata = new BeatmapMetadata() }, null, null)); + CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); } From 2f2c498477f4038315fcbcd0029bbc013ace3a60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Jan 2022 23:31:42 +0900 Subject: [PATCH 201/227] Fix importer not considering that some EF beatmaps have no local metadata --- osu.Game/Database/EFToRealmMigrator.cs | 44 +++++++++++++++----------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index ad0f2c5982..7683accc5c 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -48,6 +48,7 @@ namespace osu.Game.Database .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) .Include(s => s.Files).ThenInclude(f => f.FileInfo) + .Include(s => s.Metadata) .ToList(); // previous entries in EF are removed post migration. @@ -105,24 +106,7 @@ namespace osu.Game.Database Bookmarks = beatmap.Bookmarks, Ruleset = realm.Find(beatmap.RulesetInfo.ShortName), Difficulty = new BeatmapDifficulty(beatmap.BaseDifficulty), - Metadata = new BeatmapMetadata - { - Title = beatmap.Metadata.Title, - TitleUnicode = beatmap.Metadata.TitleUnicode, - Artist = beatmap.Metadata.Artist, - ArtistUnicode = beatmap.Metadata.ArtistUnicode, - Author = new RealmUser - { - OnlineID = beatmap.Metadata.Author.Id, - Username = beatmap.Metadata.Author.Username, - }, - Source = beatmap.Metadata.Source, - Tags = beatmap.Metadata.Tags, - PreviewTime = beatmap.Metadata.PreviewTime, - AudioFile = beatmap.Metadata.AudioFile, - BackgroundFile = beatmap.Metadata.BackgroundFile, - AuthorString = beatmap.Metadata.AuthorString, - }, + Metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata), BeatmapSet = realmBeatmapSet, }; @@ -140,6 +124,30 @@ namespace osu.Game.Database } } + private BeatmapMetadata getBestMetadata(EFBeatmapMetadata? beatmapMetadata, EFBeatmapMetadata? beatmapSetMetadata) + { + var metadata = beatmapMetadata ?? beatmapSetMetadata ?? new EFBeatmapMetadata(); + + return new BeatmapMetadata + { + Title = metadata.Title, + TitleUnicode = metadata.TitleUnicode, + Artist = metadata.Artist, + ArtistUnicode = metadata.ArtistUnicode, + Author = new RealmUser + { + OnlineID = metadata.Author.Id, + Username = metadata.Author.Username, + }, + Source = metadata.Source, + Tags = metadata.Tags, + PreviewTime = metadata.PreviewTime, + AudioFile = metadata.AudioFile, + BackgroundFile = metadata.BackgroundFile, + AuthorString = metadata.AuthorString, + }; + } + private void migrateScores(DatabaseWriteUsage db) { // can be removed 20220730. From 2984f2f6c4e7abcf75cff17c7cc92b081e48ccb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 15 Jan 2022 01:15:09 +0900 Subject: [PATCH 202/227] Fix custom keybindings not working due to incorrect use of `IsManaged` flag --- .../Bindings/DatabasedKeyBindingContainer.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 9482f1c0d8..03b069d431 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -78,19 +78,20 @@ namespace osu.Game.Input.Bindings { var defaults = DefaultKeyBindings.ToList(); - if (ruleset != null && !ruleset.IsManaged) - // some tests instantiate a ruleset which is not present in the database. - // in these cases we still want key bindings to work, but matching to database instances would result in none being present, - // so let's populate the defaults directly. + List newBindings = realmKeyBindings.Detach() + // this ordering is important to ensure that we read entries from the database in the order + // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise + // have been eaten by the music controller due to query order. + .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList(); + + // In the case no bindings were found in the database, presume this usage is for a non-databased ruleset. + // This actually should never be required and can be removed if it is ever deemed to cause a problem. + // See https://github.com/ppy/osu/issues/8805 for original reasoning, which is no longer valid as we use ShortName + // for lookups these days. + if (newBindings.Count == 0) KeyBindings = defaults; else - { - KeyBindings = realmKeyBindings.Detach() - // this ordering is important to ensure that we read entries from the database in the order - // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise - // have been eaten by the music controller due to query order. - .OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList(); - } + KeyBindings = newBindings; } } } From e75d21507c238ff6a6f5b374a301a7ccbe1f7a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Jan 2022 15:26:41 +0100 Subject: [PATCH 203/227] Fix `GetDisplayTitleRomanisable()` relying on `ToString()` implementation --- osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index 7aab6a7a9b..7e7d1babf0 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps /// public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapMetadataInfo metadataInfo, bool includeCreator = true) { - string author = !includeCreator || string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author})"; + string author = !includeCreator || string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author.Username})"; string artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode; string titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode; From 88bf406c225c3a074f2d9756b815338f42910bd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 12:56:43 +0900 Subject: [PATCH 204/227] Fix null reference in equality comparison causing beatmap set overlay to crash --- .../Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index d659df21c6..1f3f73a60a 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -107,7 +107,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { set { - if (score.Equals(value)) + if (score?.Equals(value) == true) return; score = value; From a3806f44a51adb35ad97299aa2250d1138b6ce18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 13:11:43 +0900 Subject: [PATCH 205/227] Add back `null` beatmap allowance to `GetTotalScore` flow to fix playlist aggregate scores --- osu.Game/Scoring/ScoreManager.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index ad2be7d813..dc90e819d4 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -132,9 +132,10 @@ namespace osu.Game.Scoring /// The total score. public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { - // TODO: ?? - // if (score.Beatmap == null) - // return score.TotalScore; + // TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place. + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (score.BeatmapInfo == null) + return score.TotalScore; int beatmapMaxCombo; double accuracy = score.Accuracy; From d27ee2c9fb62348c0bf24f52f7dd4b5608670080 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 13:37:42 +0900 Subject: [PATCH 206/227] Remove incorrect `IsManaged` check in `ScoreManager` --- osu.Game/Scoring/ScoreManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index dc90e819d4..52180d7ff2 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -159,11 +159,8 @@ namespace osu.Game.Scoring beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value; else { - if (!score.BeatmapInfo.IsManaged || difficulties == null) - { - // We don't have enough information (max combo) to compute the score, so use the provided score. + if (difficulties == null) return score.TotalScore; - } // We can compute the max combo locally after the async beatmap difficulty computation. var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); From 11ca1b6e7b042f0fdf3c6c4d4691a19fe9d0ae84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 13:40:27 +0900 Subject: [PATCH 207/227] Remove one more usage of `IsManaged` which could potentially go wrong --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 13ece08144..dc67a4eb45 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Select newRoot.AddChildren(beatmapSets.Select(createCarouselSet).Where(g => g != null)); root = newRoot; - if (selectedBeatmapSet != null && (!selectedBeatmapSet.BeatmapSet.IsManaged || !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))) + if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; Scroll.Clear(false); From 744084b41870cb5238263dab0a10743d241034a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 13:51:30 +0900 Subject: [PATCH 208/227] Initialise all parameters is paramaterless constructor for now for added safety --- osu.Game/Beatmaps/BeatmapInfo.cs | 9 +++++--- osu.Game/Scoring/ScoreInfo.cs | 39 +++++++++++++++++--------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index b9dd59cfe4..e14e945865 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -31,11 +31,11 @@ namespace osu.Game.Beatmaps public string DifficultyName { get; set; } = string.Empty; - public RulesetInfo Ruleset { get; set; } = null!; + public RulesetInfo Ruleset { get; set; } - public BeatmapDifficulty Difficulty { get; set; } = new BeatmapDifficulty(); + public BeatmapDifficulty Difficulty { get; set; } - public BeatmapMetadata Metadata { get; set; } = new BeatmapMetadata(); + public BeatmapMetadata Metadata { get; set; } [IgnoreMap] [Backlink(nameof(ScoreInfo.BeatmapInfo))] @@ -51,6 +51,9 @@ namespace osu.Game.Beatmaps [UsedImplicitly] public BeatmapInfo() // TODO: consider removing this and migrating all usages to ctor with parameters. { + Ruleset = new RulesetInfo(); + Difficulty = new BeatmapDifficulty(); + Metadata = new BeatmapMetadata(); } public BeatmapSetInfo? BeatmapSet { get; set; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index b168726283..fd0f14266a 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -32,19 +32,33 @@ namespace osu.Game.Scoring [PrimaryKey] public Guid ID { get; set; } = Guid.NewGuid(); + public BeatmapInfo BeatmapInfo { get; set; } + + public RulesetInfo Ruleset { get; set; } + public IList Files { get; } = null!; public string Hash { get; set; } = string.Empty; public bool DeletePending { get; set; } - public bool Equals(ScoreInfo other) => other.ID == ID; + public long TotalScore { get; set; } + + public int MaxCombo { get; set; } + + public double Accuracy { get; set; } + + public bool HasReplay { get; set; } + + public DateTimeOffset Date { get; set; } + + public double? PP { get; set; } [Indexed] public long OnlineID { get; set; } = -1; [MapTo("User")] - public RealmUser RealmUser { get; set; } = new RealmUser(); + public RealmUser RealmUser { get; set; } [MapTo("Mods")] public string ModsJson { get; set; } = string.Empty; @@ -62,6 +76,9 @@ namespace osu.Game.Scoring [UsedImplicitly] public ScoreInfo() // TODO: consider removing this and migrating all usages to ctor with parameters. { + Ruleset = new RulesetInfo(); + RealmUser = new RealmUser(); + BeatmapInfo = new BeatmapInfo(); } // TODO: this is a bit temporary to account for the fact that this class is used to ferry API user data to certain UI components. @@ -88,22 +105,6 @@ namespace osu.Game.Scoring } } - public long TotalScore { get; set; } - - public int MaxCombo { get; set; } - - public double Accuracy { get; set; } - - public bool HasReplay { get; set; } - - public DateTimeOffset Date { get; set; } - - public double? PP { get; set; } - - public BeatmapInfo BeatmapInfo { get; set; } = null!; - - public RulesetInfo Ruleset { get; set; } = null!; - public ScoreRank Rank { get => (ScoreRank)RankInt; @@ -275,6 +276,8 @@ namespace osu.Game.Scoring #endregion + public bool Equals(ScoreInfo other) => other.ID == ID; + public override string ToString() => this.GetDisplayTitle(); } } From a0e210646863b0d72a443f45a691adcf0726bf88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 14:02:15 +0900 Subject: [PATCH 209/227] Guard against null values getting inserted into database during score/beatmap imports --- osu.Game/Scoring/ScoreModelManager.cs | 5 +++++ osu.Game/Stores/BeatmapImporter.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index fcf7244226..5ba152fad3 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -63,6 +63,11 @@ namespace osu.Game.Scoring if (!model.Ruleset.IsManaged) model.Ruleset = realm.Find(model.Ruleset.ShortName); + // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed). + // Under no circumstance do we want these to be written to realm as null. + if (model.BeatmapInfo == null) throw new ArgumentNullException(nameof(model.BeatmapInfo)); + if (model.Ruleset == null) throw new ArgumentNullException(nameof(model.Ruleset)); + if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); } diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 3ebdcde296..5303bd6fba 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -64,7 +64,7 @@ namespace osu.Game.Stores // ensure we aren't trying to add a new ruleset to the database // this can happen in tests, mostly if (!b.Ruleset.IsManaged) - b.Ruleset = realm.Find(b.Ruleset.ShortName); + b.Ruleset = realm.Find(b.Ruleset.ShortName) ?? throw new ArgumentNullException(nameof(b.Ruleset)); } validateOnlineIds(beatmapSet, realm); From 381174e48248d13c241e0b9623c4a2eb199a8f4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 14:40:00 +0900 Subject: [PATCH 210/227] Give the placeholder ruleset better defaults to allow tests to work again --- osu.Game/Beatmaps/BeatmapInfo.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index e14e945865..224cf60c49 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -51,7 +51,12 @@ namespace osu.Game.Beatmaps [UsedImplicitly] public BeatmapInfo() // TODO: consider removing this and migrating all usages to ctor with parameters. { - Ruleset = new RulesetInfo(); + Ruleset = new RulesetInfo + { + OnlineID = 0, + ShortName = @"osu", + Name = @"null placeholder ruleset" + }; Difficulty = new BeatmapDifficulty(); Metadata = new BeatmapMetadata(); } @@ -149,20 +154,17 @@ namespace osu.Game.Beatmaps #region Compatibility properties - private int rulesetID; - [Ignored] [IgnoreMap] public int RulesetID { - // ReSharper disable once ConstantConditionalAccessQualifier - get => Ruleset?.OnlineID ?? rulesetID; + get => Ruleset.OnlineID; set { - if (Ruleset != null) - throw new InvalidOperationException($"Cannot set a {nameof(RulesetID)} when {nameof(Ruleset)} is non-null"); + if (!string.IsNullOrEmpty(Ruleset.InstantiationInfo)) + throw new InvalidOperationException($"Cannot set a {nameof(RulesetID)} when {nameof(Ruleset)} is already set to an actual ruleset."); - rulesetID = value; + Ruleset.OnlineID = value; } } From 90166829c3d6128c8d91d24963d51a0cb011ecd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 14:40:06 +0900 Subject: [PATCH 211/227] Update tests which were importing scores without a valid beatmap --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 22 ++++++++++++++----- .../Gameplay/TestSceneReplayDownloadButton.cs | 21 +++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 0e6d560167..dd12c94855 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -8,8 +8,8 @@ using System.Linq; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Platform; -using osu.Game.Beatmaps; using osu.Game.IO.Archives; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; @@ -17,6 +17,8 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Scores.IO { @@ -31,6 +33,8 @@ namespace osu.Game.Tests.Scores.IO { var osu = LoadOsuIntoHost(host, true); + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var toImport = new ScoreInfo { Rank = ScoreRank.B, @@ -42,7 +46,7 @@ namespace osu.Game.Tests.Scores.IO Date = DateTimeOffset.Now, OnlineID = 12345, Ruleset = new OsuRuleset().RulesetInfo, - BeatmapInfo = new BeatmapInfo() + BeatmapInfo = beatmap.Beatmaps.First() }; var imported = await LoadScoreIntoOsu(osu, toImport); @@ -71,10 +75,12 @@ namespace osu.Game.Tests.Scores.IO { var osu = LoadOsuIntoHost(host, true); + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var toImport = new ScoreInfo { User = new APIUser { Username = "Test user" }, - BeatmapInfo = new BeatmapInfo(), + BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = new OsuRuleset().RulesetInfo, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; @@ -100,10 +106,12 @@ namespace osu.Game.Tests.Scores.IO { var osu = LoadOsuIntoHost(host, true); + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var toImport = new ScoreInfo { User = new APIUser { Username = "Test user" }, - BeatmapInfo = new BeatmapInfo(), + BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = new OsuRuleset().RulesetInfo, Statistics = new Dictionary { @@ -133,10 +141,12 @@ namespace osu.Game.Tests.Scores.IO { var osu = LoadOsuIntoHost(host, true); + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + await LoadScoreIntoOsu(osu, new ScoreInfo { User = new APIUser { Username = "Test user" }, - BeatmapInfo = new BeatmapInfo(), + BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = new OsuRuleset().RulesetInfo, OnlineID = 2 }, new TestArchiveReader()); @@ -147,7 +157,7 @@ namespace osu.Game.Tests.Scores.IO Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { User = new APIUser { Username = "Test user" }, - BeatmapInfo = new BeatmapInfo(), + BeatmapInfo = beatmap.Beatmaps.First(), OnlineID = 2 })); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 3168c4b94e..8199389b36 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -6,16 +6,18 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Scoring; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; using osu.Game.Screens.Ranking; +using osu.Game.Tests.Resources; using osuTK.Input; using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; @@ -29,6 +31,18 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayDownloadButton downloadButton; + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + [Resolved] + private ScoreManager scoreManager { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + } + [Test] public void TestDisplayStates() { @@ -115,9 +129,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value); } - [Resolved] - private ScoreManager scoreManager { get; set; } - [Test] public void TestScoreImportThenDelete() { @@ -176,7 +187,7 @@ namespace osu.Game.Tests.Visual.Gameplay Id = 39828, Username = @"WubWoofWolf", } - }.CreateScoreInfo(rulesets, CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo); + }.CreateScoreInfo(rulesets, beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First()); } private class TestReplayDownloadButton : ReplayDownloadButton From 5dc7425b2cd4c44802ee8b94002790c2a08befc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Jan 2022 14:54:03 +0900 Subject: [PATCH 212/227] Fix incorrect realm (non-isolated instance) being used in two test scenes --- .../Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 3 +-- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 9ae15c387a..2e1a66be5f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -45,7 +44,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, dependencies.Get(), Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); Dependencies.Cache(ContextFactory); return dependencies; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 02746ee8e8..1e14e4b3e5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -89,7 +89,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(dependencies.Get(), () => beatmapManager, LocalStorage, dependencies.Get(), Scheduler)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); Dependencies.Cache(ContextFactory); var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); From 125439d17760b6ad2ecba6d5ac0a25d404cd5d9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 11:09:26 +0900 Subject: [PATCH 213/227] Update all (non-NET6) nuget packages --- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- .../osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 2 +- osu.Game/osu.Game.csproj | 8 ++++---- 12 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index 3c6aaa39ca..a7078f1c09 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 0719dd30df..c133b0e3f8 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index d0db43cc81..92b48470e8 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index 0719dd30df..c133b0e3f8 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 57b914bee6..a8599f2cb6 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 13f2e25f05..d5496b7479 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index d51a6da4f9..2f12b1535e 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index fea2e408f6..e5b2e070d8 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index ad3713e047..e48d80323a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 3b115d43e5..c64ef918e3 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 130fcfaca1..fb09a7be1e 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2e5c9f4548..5508cac93e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,9 +18,9 @@ - + - + @@ -35,10 +35,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + From 911a837f62b371ec7b4c119ff43af3f16c2658bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 11:09:35 +0900 Subject: [PATCH 214/227] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index b2e3b32916..18e63bb219 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5508cac93e..758575e74a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 897be33c18..d6c4533538 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - + From f9c5000774d08c3f9c7e6f561205d0c082326590 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 11:27:28 +0900 Subject: [PATCH 215/227] Remove obsoleted sentry disposal call and fix incorrectly unbound logger event --- osu.Game/Utils/SentryLogger.cs | 46 ++++++++++++++-------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 8f12760a6b..dbf04283b6 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -16,6 +16,7 @@ namespace osu.Game.Utils { private SentryClient sentry; private Scope sentryScope; + private Exception lastException; public SentryLogger(OsuGame game) { @@ -30,30 +31,27 @@ namespace osu.Game.Utils sentry = new SentryClient(options); sentryScope = new Scope(options); - Exception lastException = null; + Logger.NewEntry += processLogEntry; + } - Logger.NewEntry += entry => + private void processLogEntry(LogEntry entry) + { + if (entry.Level < LogLevel.Verbose) return; + + var exception = entry.Exception; + + if (exception != null) { - if (entry.Level < LogLevel.Verbose) return; + if (!shouldSubmitException(exception)) return; - var exception = entry.Exception; + // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports. + if (lastException != null && lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace, StringComparison.Ordinal)) return; - if (exception != null) - { - if (!shouldSubmitException(exception)) - return; - - // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports. - if (lastException != null && - lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace, StringComparison.Ordinal)) - return; - - lastException = exception; - sentry.CaptureEvent(new SentryEvent(exception) { Message = entry.Message }, sentryScope); - } - else - sentryScope.AddBreadcrumb(DateTimeOffset.Now, entry.Message, entry.Target.ToString(), "navigation"); - }; + lastException = exception; + sentry.CaptureEvent(new SentryEvent(exception) { Message = entry.Message }, sentryScope); + } + else + sentryScope.AddBreadcrumb(DateTimeOffset.Now, entry.Message, entry.Target.ToString(), "navigation"); } private bool shouldSubmitException(Exception exception) @@ -92,15 +90,9 @@ namespace osu.Game.Utils GC.SuppressFinalize(this); } - private bool isDisposed; - protected virtual void Dispose(bool isDisposing) { - if (isDisposed) - return; - - isDisposed = true; - sentry?.Dispose(); + Logger.NewEntry -= processLogEntry; sentry = null; sentryScope = null; } From c725548b9ec7b78ad08ec5beb823e70d6f4672bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 11:42:50 +0900 Subject: [PATCH 216/227] Update stray realm reference in mobile projects --- osu.Android.props | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 18e63bb219..b296c114e9 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -56,6 +56,6 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index d6c4533538..5925581e28 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -88,6 +88,6 @@ - + From a7db793d8c14f1923aeac26aeabda00a2f6aa60b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 11:39:22 +0900 Subject: [PATCH 217/227] Specify realm pipe path location I can't confirm this works yet, since I'm not sure what the preconditions are for the `.note` file to be created. What I can confirm is that a `.note` file hasn't appeared in my game data directory yet. --- osu.Game/Database/RealmContextFactory.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 96c24837a1..04253ade82 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -188,10 +189,17 @@ namespace osu.Game.Database private RealmConfiguration getConfiguration() { + // This is currently the only usage of temporary files at the osu! side. + // If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase). + string tempPathLocation = Path.Combine(Path.GetTempPath(), @"lazer"); + if (!Directory.Exists(tempPathLocation)) + Directory.CreateDirectory(tempPathLocation); + return new RealmConfiguration(storage.GetFullPath(Filename, true)) { SchemaVersion = schema_version, MigrationCallback = onMigration, + FallbackPipePath = tempPathLocation, }; } From ebc9d3a613e3e73b996f66302646de1be9ae74b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 11:57:38 +0900 Subject: [PATCH 218/227] Fix playlist aggregate scores not displaying (again) --- osu.Game/Scoring/ScoreManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 52180d7ff2..ccf3226792 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -133,8 +133,7 @@ namespace osu.Game.Scoring public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { // TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place. - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (score.BeatmapInfo == null) + if (string.IsNullOrEmpty(score.BeatmapInfo.Hash)) return score.TotalScore; int beatmapMaxCombo; From d26f4d50bd92834f021e89ac56263e426e64e6e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 13:58:12 +0900 Subject: [PATCH 219/227] Add test coverage of aggregate room scores displaying correctly --- .../TestScenePlaylistsRoomCreation.cs | 3 +++ .../Online/Leaderboards/LeaderboardScore.cs | 10 ++++---- .../OnlinePlay/TestRoomRequestsHandler.cs | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index e59884f4f4..6efdd1b8b6 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps; @@ -71,6 +72,8 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("Progress details are hidden", () => match.ChildrenOfType().FirstOrDefault()?.Parent.Alpha == 0); + AddUntilStep("Leaderboard shows two aggregate scores", () => match.ChildrenOfType().Count(s => s.ScoreText.Text != "0") == 2); + AddStep("start match", () => match.ChildrenOfType().First().TriggerClick()); AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader); } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 14eec8b388..8f4a103513 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -53,7 +53,9 @@ namespace osu.Game.Online.Leaderboards private Drawable avatar; private Drawable scoreRank; private OsuSpriteText nameLabel; - private GlowingSpriteText scoreLabel; + + public GlowingSpriteText ScoreText { get; private set; } + private Container flagBadgeContainer; private FillFlowContainer modsContainer; @@ -198,7 +200,7 @@ namespace osu.Game.Online.Leaderboards Spacing = new Vector2(5f, 0f), Children = new Drawable[] { - scoreLabel = new GlowingSpriteText + ScoreText = new GlowingSpriteText { TextColour = Color4.White, GlowColour = Color4Extensions.FromHex(@"83ccfa"), @@ -240,7 +242,7 @@ namespace osu.Game.Online.Leaderboards public override void Show() { - foreach (var d in new[] { avatar, nameLabel, scoreLabel, scoreRank, flagBadgeContainer, modsContainer }.Concat(statisticsLabels)) + foreach (var d in new[] { avatar, nameLabel, ScoreText, scoreRank, flagBadgeContainer, modsContainer }.Concat(statisticsLabels)) d.FadeOut(); Alpha = 0; @@ -262,7 +264,7 @@ namespace osu.Game.Online.Leaderboards using (BeginDelayedSequence(250)) { - scoreLabel.FadeIn(200); + ScoreText.FadeIn(200); scoreRank.FadeIn(200); using (BeginDelayedSequence(50)) diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 520f2c4585..5a0a7e71d4 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -70,6 +70,29 @@ namespace osu.Game.Tests.Visual.OnlinePlay return true; } + case GetRoomLeaderboardRequest roomLeaderboardRequest: + roomLeaderboardRequest.TriggerSuccess(new APILeaderboard + { + Leaderboard = new List + { + new APIUserScoreAggregate + { + TotalScore = 1000000, + TotalAttempts = 5, + CompletedBeatmaps = 2, + User = new APIUser { Username = "best user" } + }, + new APIUserScoreAggregate + { + TotalScore = 50, + TotalAttempts = 1, + CompletedBeatmaps = 1, + User = new APIUser { Username = "worst user" } + } + } + }); + return true; + case PartRoomRequest partRoomRequest: partRoomRequest.TriggerSuccess(); return true; From 9a43ed742b0260b29dc28d9221676ffce938f3a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 15:21:08 +0900 Subject: [PATCH 220/227] Update automapper spec in line with v11 See https://docs.automapper.org/en/latest/11.0-Upgrade-Guide.html for more details. --- osu.Game/Beatmaps/BeatmapInfo.cs | 4 -- osu.Game/Database/RealmObjectExtensions.cs | 56 +++++++++++++++------- osu.Game/Scoring/ScoreInfo.cs | 3 +- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 224cf60c49..b0e10c2c38 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using AutoMapper; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Testing; @@ -37,7 +36,6 @@ namespace osu.Game.Beatmaps public BeatmapMetadata Metadata { get; set; } - [IgnoreMap] [Backlink(nameof(ScoreInfo.BeatmapInfo))] public IQueryable Scores { get; } = null!; @@ -155,7 +153,6 @@ namespace osu.Game.Beatmaps #region Compatibility properties [Ignored] - [IgnoreMap] public int RulesetID { get => Ruleset.OnlineID; @@ -169,7 +166,6 @@ namespace osu.Game.Beatmaps } [Ignored] - [IgnoreMap] public BeatmapDifficulty BaseDifficulty { get => Difficulty; diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index a162bd64d3..4ddf0077ca 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using AutoMapper; +using AutoMapper.Internal; using osu.Framework.Development; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; @@ -60,7 +62,14 @@ namespace osu.Game.Database } }); - c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); + c.Internal().ForAllMaps((typeMap, expression) => + { + expression.ForAllMembers(m => + { + if (m.DestinationMember.Has() || m.DestinationMember.Has() || m.DestinationMember.Has()) + m.Ignore(); + }); + }); }).CreateMapper(); private static readonly IMapper mapper = new MapperConfiguration(c => @@ -80,24 +89,35 @@ namespace osu.Game.Database c.CreateMap(); c.CreateMap(); c.CreateMap(); - c.CreateMap().MaxDepth(2).AfterMap((s, d) => - { - for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++) - { - if (d.BeatmapSet.Beatmaps[i].Equals(d)) - { - d.BeatmapSet.Beatmaps[i] = d; - break; - } - } - }); - c.CreateMap().MaxDepth(2).AfterMap((s, d) => - { - foreach (var beatmap in d.Beatmaps) - beatmap.BeatmapSet = d; - }); + c.CreateMap() + .MaxDepth(2) + .AfterMap((s, d) => + { + for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++) + { + if (d.BeatmapSet.Beatmaps[i].Equals(d)) + { + d.BeatmapSet.Beatmaps[i] = d; + break; + } + } + }); + c.CreateMap() + .MaxDepth(2) + .AfterMap((s, d) => + { + foreach (var beatmap in d.Beatmaps) + beatmap.BeatmapSet = d; + }); - c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema)); + c.Internal().ForAllMaps((typeMap, expression) => + { + expression.ForAllMembers(m => + { + if (m.DestinationMember.Has() || m.DestinationMember.Has() || m.DestinationMember.Has()) + m.Ignore(); + }); + }); }).CreateMapper(); /// diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index fd0f14266a..5614f34f3f 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using AutoMapper; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Localisation; @@ -85,7 +84,7 @@ namespace osu.Game.Scoring // Eventually we should either persist enough information to realm to not require the API lookups, or perform the API lookups locally. private APIUser? user; - [IgnoreMap] + [Ignored] public APIUser User { get => user ??= new APIUser From 7084183d6c3a3a126d0c5ddc2401df16e264fab8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 15:43:02 +0900 Subject: [PATCH 221/227] Fix test beatmaps created without hash being populated --- osu.Game.Tests/Resources/TestResources.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index d0ffb9c3db..1d99a5c20d 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -130,6 +130,7 @@ namespace osu.Game.Tests.Resources StarRating = diff, Length = length, BPM = bpm, + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), Ruleset = rulesetInfo, Metadata = metadata, BaseDifficulty = new BeatmapDifficulty From a5862ca00df2e6839abdb47e5dd64f9270d704c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 15:46:27 +0900 Subject: [PATCH 222/227] Improve reliability of mod deserialisation --- osu.Game/Scoring/ScoreInfo.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 5614f34f3f..e1328d8a06 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -186,10 +186,7 @@ namespace osu.Game.Scoring if (mods != null) return mods; - if (apiMods != null) - return APIMods.Select(m => m.ToMod(Ruleset.CreateInstance())).ToArray(); - - return Array.Empty(); + return APIMods.Select(m => m.ToMod(Ruleset.CreateInstance())).ToArray(); } set { @@ -216,7 +213,7 @@ namespace osu.Game.Scoring // then check mods set via Mods property. if (mods != null) - apiMods = mods.Select(m => new APIMod(m)).ToArray(); + apiMods ??= mods.Select(m => new APIMod(m)).ToArray(); return apiMods ?? Array.Empty(); } From 8978b88f69300d868635d295104d093b0aafe76b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 16:22:11 +0900 Subject: [PATCH 223/227] Add test coverage of startup ruleset being non-default --- .../Navigation/TestSceneStartupRuleset.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs new file mode 100644 index 0000000000..85dd501fd3 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupRuleset.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Development; +using osu.Game.Configuration; + +namespace osu.Game.Tests.Visual.Navigation +{ + [TestFixture] + public class TestSceneStartupRuleset : OsuGameTestScene + { + protected override TestOsuGame CreateTestGame() + { + // Must be done in this function due to the RecycleLocalStorage call just before. + var config = DebugUtils.IsDebugBuild + ? new DevelopmentOsuConfigManager(LocalStorage) + : new OsuConfigManager(LocalStorage); + + config.SetValue(OsuSetting.Ruleset, "mania"); + config.Save(); + + return base.CreateTestGame(); + } + + [Test] + public void TestRulesetConsumed() + { + AddUntilStep("ruleset correct", () => Game.Ruleset.Value.ShortName == "mania"); + } + } +} From 8b8940439e770184fe80ce0c87d4387850575cc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 16:11:03 +0900 Subject: [PATCH 224/227] Fix starting game with non-default ruleset failing --- osu.Game/OsuGameBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8126b74418..4c6e9689d3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -451,13 +451,13 @@ namespace osu.Game private void onBeatmapChanged(ValueChangedEvent valueChangedEvent) { - if (!ThreadSafety.IsUpdateThread) + if (IsLoaded && !ThreadSafety.IsUpdateThread) throw new InvalidOperationException("Global beatmap bindable must be changed from update thread."); } private void onRulesetChanged(ValueChangedEvent r) { - if (!ThreadSafety.IsUpdateThread) + if (IsLoaded && !ThreadSafety.IsUpdateThread) throw new InvalidOperationException("Global ruleset bindable must be changed from update thread."); if (r.NewValue?.Available != true) From 488f0449249951fd204b4551b6beae7e290e43e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 16:46:14 +0900 Subject: [PATCH 225/227] Remove one more outdated comment --- osu.Game/Tests/Visual/OsuTestScene.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index c631286d11..4af8ec3a99 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -294,10 +294,6 @@ namespace osu.Game.Tests.Visual if (MusicController?.TrackLoaded == true) MusicController.Stop(); - // TODO: what should we do here, if anything? should we use an in-memory realm in this instance? - // if (contextFactory?.IsValueCreated == true) - // contextFactory.Value.ResetDatabase(); - RecycleLocalStorage(true); } From 3e9e7a8fb61e6af4279b4ee405e9aa8c42674c95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 16:48:27 +0900 Subject: [PATCH 226/227] Fix `PLaylistsResultsScreen` tests falling over due to missing beatmap --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index e9210496ca..11df115b1a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -44,6 +44,10 @@ namespace osu.Game.Tests.Visual.Playlists requestComplete = false; totalCount = 0; bindHandler(); + + // beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring. + // else the tests that rely on ordering will fall over. + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); }); [Test] From 96d07e20edad425dd505fc70ec216f7c8160b2ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Jan 2022 02:17:43 +0900 Subject: [PATCH 227/227] Revert nunit test adaptor version bump until console output bug is resolved Tests have started to output too much log content, causing viewing CI failures to be painfully impossible. Roll back for now. Fix may be related to https://github.com/nunit/nunit3-vs-adapter/issues/941, although we don't use filter. --- .../osu.Game.Rulesets.EmptyFreeform.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- .../osu.Game.Rulesets.EmptyScrolling.Tests.csproj | 2 +- .../osu.Game.Rulesets.Pippidon.Tests.csproj | 2 +- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj index a7078f1c09..3c6aaa39ca 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index c133b0e3f8..0719dd30df 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj index 92b48470e8..d0db43cc81 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj index c133b0e3f8..0719dd30df 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index a8599f2cb6..57b914bee6 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index d5496b7479..13f2e25f05 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 2f12b1535e..d51a6da4f9 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index e5b2e070d8..fea2e408f6 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index e48d80323a..ad3713e047 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index c64ef918e3..3b115d43e5 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index fb09a7be1e..130fcfaca1 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe