From c383f26729e3632a9d1e0567234435249652facc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 15:59:12 +0900 Subject: [PATCH 001/217] 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 002/217] 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 003/217] 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 004/217] 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 005/217] 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 006/217] 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 007/217] 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 008/217] 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 009/217] 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 010/217] 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 011/217] 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 012/217] 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 013/217] 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 014/217] 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 015/217] 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 016/217] 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 017/217] 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 018/217] 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 019/217] 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 020/217] 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 021/217] 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 022/217] 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 023/217] 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 024/217] 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 025/217] 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 026/217] 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 027/217] 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 028/217] 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 029/217] 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 030/217] 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 031/217] 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 032/217] 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 033/217] 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 034/217] 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 035/217] 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 036/217] 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 037/217] 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 038/217] 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 039/217] 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 040/217] "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 041/217] 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 042/217] "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 043/217] 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 044/217] 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 045/217] 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 046/217] 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 047/217] 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 048/217] 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 049/217] 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 050/217] 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 051/217] 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 052/217] 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 053/217] 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 054/217] 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 055/217] 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 056/217] 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 057/217] 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 058/217] 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 059/217] 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 060/217] 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 061/217] 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 062/217] 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 063/217] 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 064/217] 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 065/217] 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 066/217] 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 067/217] `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 068/217] 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 069/217] "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 070/217] 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 071/217] 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 072/217] 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 073/217] 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 074/217] 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 075/217] 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 076/217] 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 077/217] 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 078/217] 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 079/217] 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 080/217] 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 081/217] 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 082/217] 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 083/217] 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 084/217] 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 085/217] 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 086/217] 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 087/217] 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 088/217] 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 089/217] 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 090/217] 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 091/217] 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 092/217] 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 093/217] 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 094/217] 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 095/217] 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 096/217] 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 097/217] 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 098/217] 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 099/217] 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 100/217] 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 101/217] 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 102/217] 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 103/217] 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 104/217] 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 105/217] 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 106/217] 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 107/217] 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 108/217] 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 109/217] 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 110/217] 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 111/217] 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 112/217] 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 113/217] 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 114/217] 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 115/217] 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 116/217] 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 117/217] 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 118/217] 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 119/217] 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 120/217] 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 121/217] 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 122/217] 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 123/217] 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 124/217] 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 125/217] 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 126/217] 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 127/217] 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 128/217] 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 129/217] 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 130/217] 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 131/217] 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 132/217] 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 133/217] 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 134/217] 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 135/217] 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 136/217] 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 137/217] 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 138/217] 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 139/217] 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 140/217] 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 141/217] 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 142/217] 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 143/217] 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 144/217] 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 145/217] 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 146/217] 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 147/217] 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 148/217] 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 149/217] 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 150/217] 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 151/217] 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 152/217] 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 153/217] 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 154/217] 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 155/217] 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 156/217] 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 157/217] 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 158/217] 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 159/217] 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 160/217] 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 161/217] 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 162/217] 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 163/217] 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 164/217] 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 165/217] 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 166/217] 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 167/217] 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 168/217] 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 169/217] 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 170/217] 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 171/217] 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 172/217] 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 173/217] 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 174/217] 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 175/217] 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 176/217] 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 177/217] 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 178/217] 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 179/217] 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 180/217] 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 181/217] 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 182/217] 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 183/217] 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 184/217] 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 185/217] 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 186/217] 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 187/217] 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 188/217] 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 189/217] 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 190/217] 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 191/217] 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 192/217] 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 193/217] 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 194/217] 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 195/217] 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 196/217] 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 197/217] 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 198/217] 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 199/217] 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 200/217] 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 201/217] 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 202/217] 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 203/217] 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 204/217] 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 205/217] 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 206/217] 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 207/217] 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 208/217] 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 209/217] 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 210/217] 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 211/217] 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 ebc9d3a613e3e73b996f66302646de1be9ae74b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 11:57:38 +0900 Subject: [PATCH 212/217] 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 9a43ed742b0260b29dc28d9221676ffce938f3a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 15:21:08 +0900 Subject: [PATCH 213/217] 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 214/217] 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 215/217] 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 488f0449249951fd204b4551b6beae7e290e43e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Jan 2022 16:46:14 +0900 Subject: [PATCH 216/217] 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 217/217] 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]