From 6ca415da9f237635686504f1695b8792a719e5f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 15:25:00 +0900 Subject: [PATCH 01/29] Add basic realm models Only the file related ones are really required outside of tests, but seems like as good an opportunity as ever to get the rest of the models into the game project. --- osu.Game/Database/IHasRealmFiles.cs | 18 ++++ osu.Game/Database/INamedFile.cs | 17 +++ osu.Game/Models/RealmBeatmap.cs | 122 ++++++++++++++++++++++ osu.Game/Models/RealmBeatmapDifficulty.cs | 43 ++++++++ osu.Game/Models/RealmBeatmapMetadata.cs | 45 ++++++++ osu.Game/Models/RealmBeatmapSet.cs | 78 ++++++++++++++ osu.Game/Models/RealmFile.cs | 20 ++++ osu.Game/Models/RealmNamedFileUsage.cs | 32 ++++++ osu.Game/Models/RealmRuleset.cs | 63 +++++++++++ 9 files changed, 438 insertions(+) create mode 100644 osu.Game/Database/IHasRealmFiles.cs create mode 100644 osu.Game/Database/INamedFile.cs create mode 100644 osu.Game/Models/RealmBeatmap.cs create mode 100644 osu.Game/Models/RealmBeatmapDifficulty.cs create mode 100644 osu.Game/Models/RealmBeatmapMetadata.cs create mode 100644 osu.Game/Models/RealmBeatmapSet.cs create mode 100644 osu.Game/Models/RealmFile.cs create mode 100644 osu.Game/Models/RealmNamedFileUsage.cs create mode 100644 osu.Game/Models/RealmRuleset.cs diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs new file mode 100644 index 0000000000..2adfe73d1e --- /dev/null +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Models; + +namespace osu.Game.Database +{ + /// + /// A model that contains a list of files it is responsible for. + /// + public interface IHasRealmFiles + { + IList Files { get; } + + string Hash { get; set; } + } +} diff --git a/osu.Game/Database/INamedFile.cs b/osu.Game/Database/INamedFile.cs new file mode 100644 index 0000000000..9c94aed38c --- /dev/null +++ b/osu.Game/Database/INamedFile.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Models; + +namespace osu.Game.Database +{ + /// + /// Represent a join model which gives a filename and scope to a . + /// + public interface INamedFile + { + public string Filename { get; set; } + + public RealmFile File { get; set; } + } +} diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs new file mode 100644 index 0000000000..09f8dafeb6 --- /dev/null +++ b/osu.Game/Models/RealmBeatmap.cs @@ -0,0 +1,122 @@ +// 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 JetBrains.Annotations; +using Newtonsoft.Json; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Rulesets; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + /// + /// A single beatmap difficulty. + /// + [ExcludeFromDynamicCompile] + [Serializable] + [MapTo("Beatmap")] + public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public string DifficultyName { get; set; } = string.Empty; + + public RealmRuleset Ruleset { get; set; } = null!; + + public RealmBeatmapDifficulty Difficulty { get; set; } = null!; + + public RealmBeatmapMetadata Metadata { get; set; } = null!; + + public RealmBeatmapSet? BeatmapSet { get; set; } + + public BeatmapSetOnlineStatus Status + { + get => (BeatmapSetOnlineStatus)StatusInt; + set => StatusInt = (int)value; + } + + [MapTo(nameof(Status))] + public int StatusInt { get; set; } + + public int? OnlineID { get; set; } + + public double Length { get; set; } + + public double BPM { get; set; } + + public string Hash { get; set; } = string.Empty; + + public double StarRating { get; set; } + + public string MD5Hash { get; set; } = string.Empty; + + [JsonIgnore] + public bool Hidden { get; set; } + + public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata) + { + Ruleset = ruleset; + Difficulty = difficulty; + Metadata = metadata; + } + + [UsedImplicitly] + private RealmBeatmap() + { + } + + #region Properties we may not want persisted (but also maybe no harm?) + + public double AudioLeadIn { get; set; } + + public float StackLeniency { get; set; } = 0.7f; + + public bool SpecialStyle { get; set; } + + public bool LetterboxInBreaks { get; set; } + + public bool WidescreenStoryboard { get; set; } + + public bool EpilepsyWarning { get; set; } + + public bool SamplesMatchPlaybackRate { get; set; } + + public double DistanceSpacing { get; set; } + + public int BeatDivisor { get; set; } + + public int GridSize { get; set; } + + public double TimelineZoom { get; set; } + + #endregion + + /// + /// Returns a shallow-clone of this . + /// + public RealmBeatmap Clone() => (RealmBeatmap)MemberwiseClone(); + + public bool AudioEquals(RealmBeatmap? 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 + && 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; + } +} diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs new file mode 100644 index 0000000000..44bfdda491 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapDifficulty.cs @@ -0,0 +1,43 @@ +// 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 Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [MapTo("BeatmapDifficulty")] + public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo + { + public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + + public double SliderMultiplier { get; set; } = 1; + public double SliderTickRate { get; set; } = 1; + + /// + /// Returns a shallow-clone of this . + /// + public RealmBeatmapDifficulty Clone() + { + var diff = new RealmBeatmapDifficulty(); + CopyTo(diff); + return diff; + } + + public void CopyTo(RealmBeatmapDifficulty difficulty) + { + difficulty.ApproachRate = ApproachRate; + difficulty.DrainRate = DrainRate; + difficulty.CircleSize = CircleSize; + difficulty.OverallDifficulty = OverallDifficulty; + + difficulty.SliderMultiplier = SliderMultiplier; + difficulty.SliderTickRate = SliderTickRate; + } + } +} diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs new file mode 100644 index 0000000000..00dd120791 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapMetadata.cs @@ -0,0 +1,45 @@ +// 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.Framework.Testing; +using osu.Game.Beatmaps; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [Serializable] + [MapTo("BeatmapMetadata")] + public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo + { + public string Title { get; set; } = string.Empty; + + [JsonProperty("title_unicode")] + public string TitleUnicode { get; set; } = string.Empty; + + public string Artist { get; set; } = string.Empty; + + [JsonProperty("artist_unicode")] + public string ArtistUnicode { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User. = string.Empty; + + public string Source { get; set; } = string.Empty; + + [JsonProperty(@"tags")] + public string Tags { get; set; } = string.Empty; + + /// + /// The time in milliseconds to begin playing the track for preview purposes. + /// If -1, the track should begin playing at 40% of its length. + /// + public int PreviewTime { get; set; } + + public string AudioFile { get; set; } = string.Empty; + public string BackgroundFile { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs new file mode 100644 index 0000000000..314ca4494b --- /dev/null +++ b/osu.Game/Models/RealmBeatmapSet.cs @@ -0,0 +1,78 @@ +// 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 osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("BeatmapSet")] + public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public int? OnlineID { get; set; } + + public DateTimeOffset DateAdded { get; set; } + + public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata; + + public IList Beatmaps { get; } = null!; + + public IList Files { get; } = null!; + + public bool DeletePending { get; set; } + + public string Hash { get; set; } = string.Empty; + + /// + /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present). + /// + public bool Protected { get; set; } + + public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating); + + public double MaxLength => Beatmaps.Max(b => b.Length); + + public double MaxBPM => Beatmaps.Max(b => b.BPM); + + /// + /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. + /// The path returned is relative to the user file storage. + /// + /// 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.StoragePath; + + public override string ToString() => Metadata?.ToString() ?? base.ToString(); + + public bool Equals(RealmBeatmapSet? other) + { + if (other == null) + return false; + + if (IsManaged && other.IsManaged) + return ID == other.ID; + + if (OnlineID.HasValue && other.OnlineID.HasValue) + return OnlineID == other.OnlineID; + + if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) + return Hash == other.Hash; + + return ReferenceEquals(this, other); + } + + IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; + + IEnumerable IBeatmapSetInfo.Files => Files; + } +} diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs new file mode 100644 index 0000000000..6836d79d2d --- /dev/null +++ b/osu.Game/Models/RealmFile.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using osu.Game.IO; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [MapTo("File")] + public class RealmFile : RealmObject, IFileInfo + { + [PrimaryKey] + public string Hash { get; set; } = string.Empty; + + public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash); + } +} diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs new file mode 100644 index 0000000000..59b446112d --- /dev/null +++ b/osu.Game/Models/RealmNamedFileUsage.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Database; +using osu.Game.IO; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage + { + public RealmFile File { get; set; } = null!; + + public string Filename { get; set; } = null!; + + public RealmNamedFileUsage(RealmFile file, string filename) + { + File = file; + Filename = filename; + } + + [UsedImplicitly] + private RealmNamedFileUsage() + { + } + + IFileInfo INamedFileUsage.File => File; + } +} diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs new file mode 100644 index 0000000000..0dcd701ed2 --- /dev/null +++ b/osu.Game/Models/RealmRuleset.cs @@ -0,0 +1,63 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("Ruleset")] + public class RealmRuleset : RealmObject, IEquatable, IRulesetInfo + { + [PrimaryKey] + public string ShortName { get; set; } = string.Empty; + + public int? OnlineID { get; set; } + + public string Name { get; set; } = string.Empty; + + public string InstantiationInfo { get; set; } = string.Empty; + + public RealmRuleset(string shortName, string name, string instantiationInfo, int? onlineID = null) + { + ShortName = shortName; + Name = name; + InstantiationInfo = instantiationInfo; + OnlineID = onlineID; + } + + [UsedImplicitly] + private RealmRuleset() + { + } + + public RealmRuleset(int? onlineID, string name, string shortName, bool available) + { + OnlineID = onlineID; + Name = name; + ShortName = shortName; + Available = available; + } + + 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 override string ToString() => Name; + + public RealmRuleset Clone() => new RealmRuleset + { + OnlineID = OnlineID, + Name = Name, + ShortName = ShortName, + InstantiationInfo = InstantiationInfo, + Available = Available + }; + } +} From 03bf88ae813ad2b4026489d44fc8f9a6490936ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 15:26:16 +0900 Subject: [PATCH 02/29] Add realm `FileStore` and test coverage --- osu.Game.Tests/Database/FileStoreTests.cs | 114 ++++++++++++++++++++++ osu.Game.Tests/Database/RealmTest.cs | 42 ++++++++ osu.Game/Stores/RealmFileStore.cs | 113 +++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 osu.Game.Tests/Database/FileStoreTests.cs create mode 100644 osu.Game/Stores/RealmFileStore.cs diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs new file mode 100644 index 0000000000..861de5303d --- /dev/null +++ b/osu.Game.Tests/Database/FileStoreTests.cs @@ -0,0 +1,114 @@ +// 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 NUnit.Framework; +using osu.Framework.Logging; +using osu.Game.Models; +using osu.Game.Stores; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + public class FileStoreTests : RealmTest + { + [Test] + public void TestImportFile() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); + + realm.Write(() => files.Add(testData, realm)); + + Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8")); + Assert.True(files.Storage.Exists(realm.All().First().StoragePath)); + }); + } + + [Test] + public void TestImportSameFileTwice() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); + + realm.Write(() => files.Add(testData, realm)); + realm.Write(() => files.Add(testData, realm)); + + Assert.AreEqual(1, realm.All().Count()); + }); + } + + [Test] + public void TestDontPurgeReferenced() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); + + var timer = new Stopwatch(); + timer.Start(); + + realm.Write(() => + { + // attach the file to an arbitrary beatmap + var beatmapSet = CreateBeatmapSet(CreateRuleset()); + + beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource")); + + realm.Add(beatmapSet); + }); + + Logger.Log($"Import complete at {timer.ElapsedMilliseconds}"); + + string path = file.StoragePath; + + Assert.True(realm.All().Any()); + Assert.True(files.Storage.Exists(path)); + + files.Cleanup(); + Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}"); + + Assert.True(realm.All().Any()); + Assert.True(file.IsValid); + Assert.True(files.Storage.Exists(path)); + }); + } + + [Test] + public void TestPurgeUnreferenced() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); + + string path = file.StoragePath; + + Assert.True(realm.All().Any()); + Assert.True(files.Storage.Exists(path)); + + files.Cleanup(); + + Assert.False(realm.All().Any()); + Assert.False(file.IsValid); + Assert.False(files.Storage.Exists(path)); + }); + } + } +} diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index f5752aa606..04c9f2577a 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -5,10 +5,12 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Models; #nullable enable @@ -70,6 +72,46 @@ namespace osu.Game.Tests.Database } } + protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset) + { + RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() }; + + var metadata = new RealmBeatmapMetadata + { + Title = "My Love", + Artist = "Kuba Oms" + }; + + var beatmapSet = new RealmBeatmapSet + { + 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", } + }, + Files = + { + new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"), + } + }; + + for (int i = 0; i < 8; i++) + beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3")); + + foreach (var b in beatmapSet.Beatmaps) + b.BeatmapSet = beatmapSet; + + return beatmapSet; + } + + protected static RealmRuleset CreateRuleset() => + new RealmRuleset(0, "osu!", "osu", true); + private class RealmTestGame : Framework.Game { public RealmTestGame(Func work) diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs new file mode 100644 index 0000000000..aac52b193c --- /dev/null +++ b/osu.Game/Stores/RealmFileStore.cs @@ -0,0 +1,113 @@ +// 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.Models; +using Realms; + +#nullable enable + +namespace osu.Game.Stores +{ + /// + /// Handles the Store and retrieval of Files/FileSets to the database backing + /// + public class RealmFileStore + { + private readonly RealmContextFactory realmFactory; + public readonly IResourceStore Store; + + public Storage Storage; + + public RealmFileStore(RealmContextFactory realmFactory, Storage storage) + { + this.realmFactory = realmFactory; + + Storage = storage.GetStorageForDirectory(@"files"); + Store = new StorageBackedResourceStore(Storage); + } + + /// + /// Add a new file to the game-wide database, copying it to permanent storage if not already present. + /// + /// The file data stream. + /// The realm instance to add to. Should already be in a transaction. + /// + public RealmFile Add(Stream data, Realm realm) + { + string hash = data.ComputeSHA2Hash(); + + var existing = realm.Find(hash); + + var file = existing ?? new RealmFile { Hash = hash }; + + if (!checkFileExistsAndMatchesHash(file)) + copyToStore(file, data); + + if (!file.IsManaged) + realm.Add(file); + + return file; + } + + private void copyToStore(RealmFile file, Stream data) + { + data.Seek(0, SeekOrigin.Begin); + + using (var output = Storage.GetStream(file.StoragePath, FileAccess.Write)) + data.CopyTo(output); + + data.Seek(0, SeekOrigin.Begin); + } + + private bool checkFileExistsAndMatchesHash(RealmFile file) + { + string path = file.StoragePath; + + // we may be re-adding a file to fix missing store entries. + if (!Storage.Exists(path)) + return false; + + // even if the file already exists, check the existing checksum for safety. + using (var stream = Storage.GetStream(path)) + return stream.ComputeSHA2Hash() == file.Hash; + } + + public void Cleanup() + { + var realm = realmFactory.Context; + + // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. + using (var transaction = realm.BeginWrite()) + { + // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) + var files = realm.All().ToList(); + + foreach (var file in files) + { + if (file.BacklinksCount > 0) + continue; + + try + { + Storage.Delete(file.StoragePath); + realm.Remove(file); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete databased file {file.Hash}"); + } + } + + transaction.Commit(); + } + } + } +} From b01d82b3fd8e05548a1eee87cc33c7b5920649df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 23:46:16 +0900 Subject: [PATCH 03/29] Add `RealmLive` implementation --- osu.Game.Tests/Database/RealmLiveTests.cs | 199 +++++++++++++++++++++ osu.Game/Database/RealmLive.cs | 111 ++++++++++++ osu.Game/Database/RealmObjectExtensions.cs | 13 ++ 3 files changed, 323 insertions(+) create mode 100644 osu.Game.Tests/Database/RealmLiveTests.cs create mode 100644 osu.Game/Database/RealmLive.cs diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs new file mode 100644 index 0000000000..d6ea24e848 --- /dev/null +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -0,0 +1,199 @@ +// 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 System.Threading.Tasks; +using NUnit.Framework; +using osu.Game.Database; +using osu.Game.Models; +using Realms; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + public class RealmLiveTests : RealmTest + { + [Test] + public void TestValueAccessWithOpenContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + Assert.DoesNotThrow(() => + { + using (realmFactory.CreateContext()) + { + var resolved = liveBeatmap.Value; + + Assert.IsTrue(resolved.Realm.IsClosed); + Assert.IsTrue(resolved.IsValid); + + // can access properties without a crash. + Assert.IsFalse(resolved.Hidden); + } + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestScopedReadWithoutContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + liveBeatmap.PerformRead(beatmap => + { + Assert.IsTrue(beatmap.IsValid); + Assert.IsFalse(beatmap.Hidden); + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestScopedWriteWithoutContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; }); + liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestValueAccessWithoutOpenContextFails() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + Assert.Throws(() => + { + var unused = liveBeatmap.Value; + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestLiveAssumptions() + { + RunTestWithRealm((realmFactory, _) => + { + int changesTriggered = 0; + + using (var updateThreadContext = realmFactory.CreateContext()) + { + updateThreadContext.All().SubscribeForNotifications(gotChange); + RealmLive? 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()))); + + // 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()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + // not yet seen by main context + Assert.AreEqual(0, updateThreadContext.All().Count()); + Assert.AreEqual(0, changesTriggered); + + var resolved = liveBeatmap.Value; + + // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. + Assert.AreEqual(2, updateThreadContext.All().Count()); + Assert.AreEqual(1, changesTriggered); + + // even though the realm that this instance was resolved for was closed, it's still valid. + Assert.IsTrue(resolved.Realm.IsClosed); + Assert.IsTrue(resolved.IsValid); + + // can access properties without a crash. + Assert.IsFalse(resolved.Hidden); + + updateThreadContext.Write(r => + { + // can use with the main context. + r.Remove(resolved); + }); + } + + void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) + { + changesTriggered++; + } + }); + } + } +} diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs new file mode 100644 index 0000000000..71fb44f617 --- /dev/null +++ b/osu.Game/Database/RealmLive.cs @@ -0,0 +1,111 @@ +// 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; +using Realms; + +#nullable enable + +namespace osu.Game.Database +{ + /// + /// Provides a method of working with realm objects over longer application lifetimes. + /// + /// The underlying object type. + public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; } + + private readonly SynchronizationContext? fetchedContext; + private readonly int fetchedThreadId; + + /// + /// The original live data used to create this instance. + /// + private readonly T data; + + /// + /// Construct a new instance of live realm data. + /// + /// The realm data. + public RealmLive(T data) + { + this.data = data; + + fetchedContext = SynchronizationContext.Current; + fetchedThreadId = Thread.CurrentThread.ManagedThreadId; + + ID = data.ID; + } + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + public void PerformRead(Action perform) + { + if (originalDataValid) + { + perform(data); + return; + } + + using (var realm = Realm.GetInstance(data.Realm.Config)) + perform(realm.Find(ID)); + } + + /// + /// Perform a read operation on this live object. + /// + /// 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)}."); + + if (originalDataValid) + return perform(data); + + using (var realm = Realm.GetInstance(data.Realm.Config)) + return perform(realm.Find(ID)); + } + + /// + /// Perform a write operation on this live object. + /// + /// The action to perform. + public void PerformWrite(Action perform) => + PerformRead(t => + { + var transaction = t.Realm.BeginWrite(); + perform(t); + transaction.Commit(); + }); + + public T Value + { + get + { + if (originalDataValid) + return data; + + T retrieved; + + using (var realm = Realm.GetInstance(data.Realm.Config)) + retrieved = realm.Find(ID); + + if (!retrieved.IsValid) + throw new InvalidOperationException("Attempted to access value without an open context"); + + return retrieved; + } + } + + private bool originalDataValid => isCorrectThread && data.IsValid && !data.Realm.IsClosed; + + // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) + private bool isCorrectThread + => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId; + } +} diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index c5aa1399a3..18a926fa8c 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using AutoMapper; using osu.Game.Input.Bindings; using Realms; @@ -47,5 +48,17 @@ namespace osu.Game.Database return mapper.Map(item); } + + public static List> ToLive(this IEnumerable realmList) + where T : RealmObject, IHasGuidPrimaryKey + { + return realmList.Select(l => new RealmLive(l)).ToList(); + } + + public static RealmLive ToLive(this T realmObject) + where T : RealmObject, IHasGuidPrimaryKey + { + return new RealmLive(realmObject); + } } } From 81a0fbfc40d2a6afca48fb64141eecabf5aab011 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 14:30:27 +0900 Subject: [PATCH 04/29] Add `Live<>` casting test --- osu.Game.Tests/Database/RealmLiveTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index d6ea24e848..33aa1afb89 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Models; using Realms; @@ -16,6 +17,19 @@ namespace osu.Game.Tests.Database { public class RealmLiveTests : RealmTest { + [Test] + public void TestLiveCastability() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(); + + ILive iBeatmap = beatmap; + + Assert.AreEqual(0, iBeatmap.Value.Length); + }); + } + [Test] public void TestValueAccessWithOpenContext() { From b1ad3161dd33119d57cf42715aae1f74a1157be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Oct 2021 21:25:02 +0200 Subject: [PATCH 05/29] Add failing test case for frame stable clock direction flip scenario --- .../TestSceneFrameStabilityContainer.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 5eb71e92c2..881e3f097c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -103,6 +103,30 @@ namespace osu.Game.Tests.Visual.Gameplay checkFrameCount(0); } + [Test] + public void TestSeekToSameTimePreservesRate() + { + AddStep("set manual clock rate", () => manualClock.Rate = 1); + seekManualTo(5000); + createStabilityContainer(); + checkRate(1); + + seekManualTo(10000); + checkRate(1); + + seekManualTo(10000); + checkRate(1); + + seekManualTo(5000); + checkRate(-1); + + seekManualTo(5000); + checkRate(-1); + + seekManualTo(10000); + checkRate(1); + } + private const int max_frames_catchup = 50; private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () => @@ -116,6 +140,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void checkFrameCount(int frames) => AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames); + private void checkRate(double rate) => + AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate); + public class ClockConsumingChild : CompositeDrawable { private readonly OsuSpriteText text; From 56eae703fed67c5694004bf4d33d24b3ccb8dbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Oct 2021 21:39:48 +0200 Subject: [PATCH 06/29] Avoid changing frame stable clock direction if time hasn't changed between frames --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index e9865f6c8b..c0b339a231 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.UI /// /// The current direction of playback to be exposed to frame stable children. /// - private int direction; + /// + /// Initially it is presumed that playback will proceed in the forward direction. + /// + private int direction = 1; [BackgroundDependencyLoader(true)] private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler) @@ -139,7 +142,9 @@ namespace osu.Game.Rulesets.UI state = PlaybackState.NotValid; } - if (state == PlaybackState.Valid) + // if the proposed time is the same as the current time, assume that the clock will continue progressing in the same direction as previously. + // this avoids spurious flips in direction from -1 to 1 during rewinds. + if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); From 129416835f2e7d78d7cbf5b120c7d9708de953f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 15:40:12 +0900 Subject: [PATCH 07/29] Remove stray `string.Empty` specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Models/RealmBeatmapMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs index 00dd120791..6ea7170d0f 100644 --- a/osu.Game/Models/RealmBeatmapMetadata.cs +++ b/osu.Game/Models/RealmBeatmapMetadata.cs @@ -26,7 +26,7 @@ namespace osu.Game.Models [JsonProperty("artist_unicode")] public string ArtistUnicode { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User. = string.Empty; + public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User. public string Source { get; set; } = string.Empty; From ce128476ae59d1011bf17fac4699ddfcb8f8a59f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 15:46:32 +0900 Subject: [PATCH 08/29] Remove public setter of `RealmFileStore.Storage` --- osu.Game/Stores/RealmFileStore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index aac52b193c..5082b2c65b 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -17,14 +17,15 @@ using Realms; namespace osu.Game.Stores { /// - /// Handles the Store and retrieval of Files/FileSets to the database backing + /// Handles the storing of files to the file system (and database) backing. /// public class RealmFileStore { private readonly RealmContextFactory realmFactory; + public readonly IResourceStore Store; - public Storage Storage; + public readonly Storage Storage; public RealmFileStore(RealmContextFactory realmFactory, Storage storage) { From 76c64751de3007a21f6092359c60e4dbae4f0aaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 16:18:12 +0900 Subject: [PATCH 09/29] Remove `RealmBeatmap.Clone` for the time being (incorrectly implemented) --- osu.Game/Models/RealmBeatmap.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs index 09f8dafeb6..5049c1384d 100644 --- a/osu.Game/Models/RealmBeatmap.cs +++ b/osu.Game/Models/RealmBeatmap.cs @@ -97,11 +97,6 @@ namespace osu.Game.Models #endregion - /// - /// Returns a shallow-clone of this . - /// - public RealmBeatmap Clone() => (RealmBeatmap)MemberwiseClone(); - public bool AudioEquals(RealmBeatmap? other) => other != null && BeatmapSet != null && other.BeatmapSet != null From d1b0dd1f6b72112aa10c18f9106b6b08a14b8d48 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 12 Oct 2021 17:13:36 +0900 Subject: [PATCH 10/29] Fix GameHosts being run on TPL threads --- osu.Game.Tests/ImportTest.cs | 2 +- osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index e888f51e98..dbeb453d4d 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false) { var osu = new TestOsuGameBase(withBeatmap); - Task.Run(() => host.Run(osu)) + Task.Factory.StartNew(() => host.Run(osu), TaskCreationOptions.LongRunning) .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); diff --git a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs index b14684200f..319a768e65 100644 --- a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.NonVisual public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null) { tournament ??= new TournamentGameBase(); - Task.Run(() => host.Run(tournament)) + Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning) .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time"); return tournament; From 8dcfc3dd7e2608ef1bc715a546dab144e1d9fab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 12 Oct 2021 18:31:20 +0200 Subject: [PATCH 11/29] Replace no-op seeks with wait steps --- .../Visual/Gameplay/TestSceneFrameStabilityContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 881e3f097c..ae0decaee1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestSeekToSameTimePreservesRate() + public void TestRatePreservedWhenTimeNotProgressing() { AddStep("set manual clock rate", () => manualClock.Rate = 1); seekManualTo(5000); @@ -114,13 +114,13 @@ namespace osu.Game.Tests.Visual.Gameplay seekManualTo(10000); checkRate(1); - seekManualTo(10000); + AddWaitStep("wait some", 3); checkRate(1); seekManualTo(5000); checkRate(-1); - seekManualTo(5000); + AddWaitStep("wait some", 3); checkRate(-1); seekManualTo(10000); From 94de24075e52d154ca042a8bf9116f5f434d8e23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 12:18:56 +0900 Subject: [PATCH 12/29] Ensure startup imports trigger notifications --- osu.Game/OsuGame.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7895715045..020cdebab6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -211,13 +211,6 @@ namespace osu.Game [BackgroundDependencyLoader] private void load() { - if (args?.Length > 0) - { - var paths = args.Where(a => !a.StartsWith('-')).ToArray(); - if (paths.Length > 0) - Task.Run(() => Import(paths)); - } - dependencies.CacheAs(this); dependencies.Cache(SentryLogger); @@ -867,6 +860,19 @@ namespace osu.Game { if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); }; + + // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup. + handleStartupImport(); + } + + private void handleStartupImport() + { + if (args?.Length > 0) + { + var paths = args.Where(a => !a.StartsWith('-')).ToArray(); + if (paths.Length > 0) + Task.Run(() => Import(paths)); + } } private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays) From f69a56a26ac5debb0e365332e9f623fedb5fa937 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 12:19:10 +0900 Subject: [PATCH 13/29] Add test coverage of startup import sequence --- .../Navigation/TestSceneStartupImport.cs | 31 +++++++++++++++++++ osu.Game/Database/ArchiveModelManager.cs | 4 +-- .../Database/ImportProgressNotification.cs | 15 +++++++++ osu.Game/Tests/Visual/OsuGameTestScene.cs | 7 +++-- 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs create mode 100644 osu.Game/Database/ImportProgressNotification.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs new file mode 100644 index 0000000000..cb7c334656 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneStartupImport : OsuGameTestScene + { + private string importFilename; + + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename }); + + public override void SetUpSteps() + { + AddStep("Prepare import beatmap", () => importFilename = TestResources.GetTestBeatmapForImport()); + + base.SetUpSteps(); + } + + [Test] + public void TestImportCreatedNotification() + { + AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1); + } + } +} diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ee1a7e2900..c235fc7728 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -116,7 +116,7 @@ namespace osu.Game.Database /// One or more archive locations on disk. public Task Import(params string[] paths) { - var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + var notification = new ImportProgressNotification(); PostNotification?.Invoke(notification); @@ -125,7 +125,7 @@ namespace osu.Game.Database public Task Import(params ImportTask[] tasks) { - var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + var notification = new ImportProgressNotification(); PostNotification?.Invoke(notification); diff --git a/osu.Game/Database/ImportProgressNotification.cs b/osu.Game/Database/ImportProgressNotification.cs new file mode 100644 index 0000000000..aaee3e117f --- /dev/null +++ b/osu.Game/Database/ImportProgressNotification.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + public class ImportProgressNotification : ProgressNotification + { + public ImportProgressNotification() + { + State = ProgressNotificationState.Active; + } + } +} diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 77db697cb6..6a11bd3fea 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -78,9 +78,11 @@ namespace osu.Game.Tests.Visual protected void CreateGame() { - AddGame(Game = new TestOsuGame(LocalStorage, API)); + AddGame(Game = CreateTestGame()); } + protected virtual TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API); + protected void PushAndConfirm(Func newScreen) { Screen screen = null; @@ -135,7 +137,8 @@ namespace osu.Game.Tests.Visual public new void PerformFromScreen(Action action, IEnumerable validScreens = null) => base.PerformFromScreen(action, validScreens); - public TestOsuGame(Storage storage, IAPIProvider api) + public TestOsuGame(Storage storage, IAPIProvider api, string[] args = null) + : base(args) { Storage = storage; API = api; From cf10239e70408533a0138f9afb5a45ee4e615e99 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 12:51:41 +0900 Subject: [PATCH 14/29] Add a few nullabilities and DCC excludes --- osu.Game/Database/IHasRealmFiles.cs | 2 ++ osu.Game/Database/INamedFile.cs | 8 +++++--- osu.Game/Models/RealmBeatmapDifficulty.cs | 2 ++ osu.Game/Models/RealmFile.cs | 2 ++ osu.Game/Models/RealmNamedFileUsage.cs | 2 ++ osu.Game/Stores/RealmFileStore.cs | 2 ++ 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs index 2adfe73d1e..024d9f2a89 100644 --- a/osu.Game/Database/IHasRealmFiles.cs +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using osu.Game.Models; +#nullable enable + namespace osu.Game.Database { /// diff --git a/osu.Game/Database/INamedFile.cs b/osu.Game/Database/INamedFile.cs index 9c94aed38c..2bd45d4e42 100644 --- a/osu.Game/Database/INamedFile.cs +++ b/osu.Game/Database/INamedFile.cs @@ -3,15 +3,17 @@ using osu.Game.Models; +#nullable enable + namespace osu.Game.Database { /// - /// Represent a join model which gives a filename and scope to a . + /// Represents a join model which gives a filename and scope to a . /// public interface INamedFile { - public string Filename { get; set; } + string Filename { get; set; } - public RealmFile File { get; set; } + RealmFile File { get; set; } } } diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs index 44bfdda491..3c1dad69e4 100644 --- a/osu.Game/Models/RealmBeatmapDifficulty.cs +++ b/osu.Game/Models/RealmBeatmapDifficulty.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 osu.Framework.Testing; using osu.Game.Beatmaps; using Realms; @@ -8,6 +9,7 @@ using Realms; namespace osu.Game.Models { + [ExcludeFromDynamicCompile] [MapTo("BeatmapDifficulty")] public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo { diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs index 6836d79d2d..2715f4be45 100644 --- a/osu.Game/Models/RealmFile.cs +++ b/osu.Game/Models/RealmFile.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using osu.Framework.Testing; using osu.Game.IO; using Realms; @@ -9,6 +10,7 @@ using Realms; namespace osu.Game.Models { + [ExcludeFromDynamicCompile] [MapTo("File")] public class RealmFile : RealmObject, IFileInfo { diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs index 59b446112d..ba12d51d0b 100644 --- a/osu.Game/Models/RealmNamedFileUsage.cs +++ b/osu.Game/Models/RealmNamedFileUsage.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using JetBrains.Annotations; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.IO; using Realms; @@ -10,6 +11,7 @@ using Realms; namespace osu.Game.Models { + [ExcludeFromDynamicCompile] public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage { public RealmFile File { get; set; } = null!; diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index 5082b2c65b..f7b7471634 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Models; using Realms; @@ -19,6 +20,7 @@ namespace osu.Game.Stores /// /// Handles the storing of files to the file system (and database) backing. /// + [ExcludeFromDynamicCompile] public class RealmFileStore { private readonly RealmContextFactory realmFactory; From b37096f44062fbff4b4dd9e23708af55ffb4f249 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 13:25:30 +0900 Subject: [PATCH 15/29] Avoid using bindable for `AudioFilter` cutoff It doesn't play nicely with screen exiting, as it is automatically unbound during the exit process. Easiest to just avoid using this for now. --- .../Visual/Audio/TestSceneAudioFilter.cs | 27 +++++++--- osu.Game/Audio/Effects/AudioFilter.cs | 51 ++++++++++--------- .../Audio/Effects/ITransformableFilter.cs | 7 ++- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs index 211543a881..851e0eb2a1 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs @@ -34,6 +34,9 @@ namespace osu.Game.Tests.Visual.Audio beatmap = new WaveformTestBeatmap(audio); track = beatmap.LoadTrack(); + OsuSliderBar lowPassCutoff; + OsuSliderBar highPassCutoff; + Add(new FillFlowContainer { Children = new Drawable[] @@ -43,33 +46,41 @@ namespace osu.Game.Tests.Visual.Audio lowpassText = new OsuSpriteText { Padding = new MarginPadding(20), - Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz", + Text = $"Low Pass: {lowpassFilter.Cutoff}hz", Font = new FontUsage(size: 40) }, - new OsuSliderBar + lowPassCutoff = new OsuSliderBar { Width = 500, Height = 50, Padding = new MarginPadding(20), - Current = { BindTarget = lowpassFilter.Cutoff } }, highpassText = new OsuSpriteText { Padding = new MarginPadding(20), - Text = $"High Pass: {highpassFilter.Cutoff.Value}hz", + Text = $"High Pass: {highpassFilter.Cutoff}hz", Font = new FontUsage(size: 40) }, - new OsuSliderBar + highPassCutoff = new OsuSliderBar { Width = 500, Height = 50, Padding = new MarginPadding(20), - Current = { BindTarget = highpassFilter.Cutoff } } } }); - lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz"; - highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz"; + + lowPassCutoff.Current.ValueChanged += e => + { + lowpassText.Text = $"Low Pass: {e.NewValue}hz"; + lowpassFilter.Cutoff = e.NewValue; + }; + + highPassCutoff.Current.ValueChanged += e => + { + highpassText.Text = $"High Pass: {e.NewValue}hz"; + highpassFilter.Cutoff = e.NewValue; + }; } [SetUpSteps] diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index ee48bdd7d9..0152254945 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using ManagedBass.Fx; using osu.Framework.Audio.Mixing; -using osu.Framework.Bindables; using osu.Framework.Graphics; namespace osu.Game.Audio.Effects @@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects private readonly BQFParameters filter; private readonly BQFType type; + private int cutoff; + /// - /// The current cutoff of this filter. + /// The cutoff frequency of this filter. /// - public BindableNumber Cutoff { get; } + public int Cutoff + { + get => cutoff; + set + { + if (value == cutoff) + return; + + int oldValue = cutoff; + cutoff = value; + + updateFilter(oldValue, cutoff); + } + } /// /// A Component that implements a BASS FX BiQuad Filter Effect. @@ -36,33 +50,25 @@ namespace osu.Game.Audio.Effects this.mixer = mixer; this.type = type; - int initialCutoff; - switch (type) { case BQFType.HighPass: - initialCutoff = 1; + cutoff = 1; break; case BQFType.LowPass: - initialCutoff = MAX_LOWPASS_CUTOFF; + cutoff = MAX_LOWPASS_CUTOFF; break; default: - initialCutoff = 500; // A default that should ensure audio remains audible for other filters. + cutoff = 500; // A default that should ensure audio remains audible for other filters. break; } - Cutoff = new BindableNumber(initialCutoff) - { - MinValue = 1, - MaxValue = MAX_LOWPASS_CUTOFF - }; - filter = new BQFParameters { lFilter = type, - fCenter = initialCutoff, + fCenter = cutoff, fBandwidth = 0, fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) }; @@ -70,8 +76,6 @@ namespace osu.Game.Audio.Effects // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic) if (type != BQFType.LowPass && type != BQFType.HighPass) attachFilter(); - - Cutoff.ValueChanged += updateFilter; } private void attachFilter() @@ -86,40 +90,41 @@ namespace osu.Game.Audio.Effects mixer.Effects.Remove(filter); } - private void updateFilter(ValueChangedEvent cutoff) + private void updateFilter(int oldValue, int newValue) { // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. if (type == BQFType.LowPass) { - if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF) + if (newValue >= MAX_LOWPASS_CUTOFF) { detachFilter(); return; } - if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF) + if (oldValue >= MAX_LOWPASS_CUTOFF) attachFilter(); } // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. if (type == BQFType.HighPass) { - if (cutoff.NewValue <= 1) + if (newValue <= 1) { detachFilter(); return; } - if (cutoff.OldValue <= 1 && cutoff.NewValue > 1) + if (oldValue <= 1) attachFilter(); } var filterIndex = mixer.Effects.IndexOf(filter); + if (filterIndex < 0) return; if (mixer.Effects[filterIndex] is BQFParameters existingFilter) { - existingFilter.fCenter = cutoff.NewValue; + existingFilter.fCenter = newValue; // required to update effect with new parameters. mixer.Effects[filterIndex] = existingFilter; diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs index e4de4cf8ff..fb6a924f68 100644 --- a/osu.Game/Audio/Effects/ITransformableFilter.cs +++ b/osu.Game/Audio/Effects/ITransformableFilter.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; @@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects /// /// The filter cutoff. /// - BindableNumber Cutoff { get; } + int Cutoff { get; set; } } public static class FilterableAudioComponentExtensions @@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing) where T : class, ITransformableFilter, IDrawable where TEasing : IEasingFunction - => component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing); + => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing); /// /// Smoothly adjusts filter cutoff over time. @@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration, TEasing easing) where T : class, ITransformableFilter, IDrawable where TEasing : IEasingFunction - => sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing)); + => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing)); } } From ae4dcbd8297b2803ca1df71c33722118a4a99134 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 13:26:20 +0900 Subject: [PATCH 16/29] Improve `PlayerLoader` audio and visual transitions --- osu.Game/Screens/Play/PlayerLoader.cs | 71 ++++++++++++++++----------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 94a61a4ef3..cf5bff57cf 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play { protected const float BACKGROUND_BLUR = 15; + private const double content_out_duration = 300; + public override bool HideOverlaysOnEnter => hideOverlays; public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -135,36 +137,39 @@ namespace osu.Game.Screens.Play muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); - InternalChild = (content = new LogoTrackingContainer + InternalChildren = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }).WithChildren(new Drawable[] - { - MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade) + (content = new LogoTrackingContainer { - Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, - PlayerSettings = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + }).WithChildren(new Drawable[] { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding(25), - Children = new PlayerSettingsGroup[] + MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade) { - VisualSettings = new VisualSettings(), - new InputSettings() - } - }, - idleTracker = new IdleTracker(750), + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + PlayerSettings = new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding(25), + Children = new PlayerSettingsGroup[] + { + VisualSettings = new VisualSettings(), + new InputSettings() + } + }, + idleTracker = new IdleTracker(750), + }), lowPassFilter = new AudioFilter(audio.TrackMixer) - }); + }; if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) { @@ -195,7 +200,6 @@ namespace osu.Game.Screens.Play epilepsyWarning.DimmableBackground = b; }); - lowPassFilter.CutoffTo(500, 100, Easing.OutCubic); Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); content.ScaleTo(0.7f); @@ -240,15 +244,15 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { cancelLoad(); + contentOut(); - content.ScaleTo(0.7f, 150, Easing.InQuint); - this.FadeOut(150); + // Ensure the screen doesn't expire until all the outwards fade operations have completed. + this.Delay(content_out_duration).FadeOut(); ApplyToBackground(b => b.IgnoreUserSettings.Value = true); BackgroundBrightnessReduction = false; Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); - lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); return base.OnExiting(next); } @@ -344,6 +348,7 @@ namespace osu.Game.Screens.Play content.FadeInFromZero(400); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint)); } @@ -353,8 +358,9 @@ namespace osu.Game.Screens.Play // Ensure the logo is no longer tracking before we scale the content content.StopTracking(); - content.ScaleTo(0.7f, 300, Easing.InQuint); - content.FadeOut(250); + content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint); + content.FadeOut(content_out_duration, Easing.OutQuint); + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration); } private void pushWhenLoaded() @@ -381,7 +387,7 @@ namespace osu.Game.Screens.Play contentOut(); - TransformSequence pushSequence = this.Delay(250); + TransformSequence pushSequence = this.Delay(content_out_duration); // only show if the warning was created (i.e. the beatmap needs it) // and this is not a restart of the map (the warning expires after first load). @@ -400,6 +406,11 @@ namespace osu.Game.Screens.Play }) .Delay(EpilepsyWarning.FADE_DURATION); } + else + { + // This goes hand-in-hand with the restoration of low pass filter in contentOut(). + this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic); + } pushSequence.Schedule(() => { From 26a1e40d2471a9d4e7e44ea0f98b1b1fbc6231c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 13:47:49 +0900 Subject: [PATCH 17/29] Fix storyboard outro during fail test not being lenient enough --- .../Visual/Gameplay/TestSceneStoryboardWithOutro.cs | 6 +++++- osu.Game/Screens/Play/FailAnimation.cs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 3ed274690e..48a97d54f7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -90,8 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay CreateTest(() => { AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true); - AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300); + + // Fail occurs at 164ms with the provided beatmap. + // Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience. + AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); + AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index e250791b72..ea158c5789 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play { /// /// Manage the animation to be applied when a player fails. - /// Single file; automatically disposed after use. + /// Single use and automatically disposed after use. /// public class FailAnimation : CompositeDrawable { From 29dfe33465aa61ba7b7866b4e136b7b11ce0fe84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 15:13:35 +0900 Subject: [PATCH 18/29] Rewrite `AudioFilter` to be easier to follow (and fix tests) --- .../Visual/Audio/TestSceneAudioFilter.cs | 83 ++++++++---- osu.Game/Audio/Effects/AudioFilter.cs | 123 +++++++++--------- 2 files changed, 119 insertions(+), 87 deletions(-) diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs index 851e0eb2a1..0107632f6e 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -18,84 +19,112 @@ namespace osu.Game.Tests.Visual.Audio { public class TestSceneAudioFilter : OsuTestScene { - private OsuSpriteText lowpassText; - private AudioFilter lowpassFilter; + private OsuSpriteText lowPassText; + private AudioFilter lowPassFilter; - private OsuSpriteText highpassText; - private AudioFilter highpassFilter; + private OsuSpriteText highPassText; + private AudioFilter highPassFilter; private Track track; private WaveformTestBeatmap beatmap; + private OsuSliderBar lowPassSlider; + private OsuSliderBar highPassSlider; + [BackgroundDependencyLoader] private void load(AudioManager audio) { beatmap = new WaveformTestBeatmap(audio); track = beatmap.LoadTrack(); - OsuSliderBar lowPassCutoff; - OsuSliderBar highPassCutoff; - Add(new FillFlowContainer { Children = new Drawable[] { - lowpassFilter = new AudioFilter(audio.TrackMixer), - highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), - lowpassText = new OsuSpriteText + lowPassFilter = new AudioFilter(audio.TrackMixer), + highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), + lowPassText = new OsuSpriteText { Padding = new MarginPadding(20), - Text = $"Low Pass: {lowpassFilter.Cutoff}hz", + Text = $"Low Pass: {lowPassFilter.Cutoff}hz", Font = new FontUsage(size: 40) }, - lowPassCutoff = new OsuSliderBar + lowPassSlider = new OsuSliderBar { Width = 500, Height = 50, Padding = new MarginPadding(20), + Current = new BindableInt + { + MinValue = 0, + MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF, + } }, - highpassText = new OsuSpriteText + highPassText = new OsuSpriteText { Padding = new MarginPadding(20), - Text = $"High Pass: {highpassFilter.Cutoff}hz", + Text = $"High Pass: {highPassFilter.Cutoff}hz", Font = new FontUsage(size: 40) }, - highPassCutoff = new OsuSliderBar + highPassSlider = new OsuSliderBar { Width = 500, Height = 50, Padding = new MarginPadding(20), + Current = new BindableInt + { + MinValue = 0, + MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF, + } } } }); - lowPassCutoff.Current.ValueChanged += e => + lowPassSlider.Current.ValueChanged += e => { - lowpassText.Text = $"Low Pass: {e.NewValue}hz"; - lowpassFilter.Cutoff = e.NewValue; + lowPassText.Text = $"Low Pass: {e.NewValue}hz"; + lowPassFilter.Cutoff = e.NewValue; }; - highPassCutoff.Current.ValueChanged += e => + highPassSlider.Current.ValueChanged += e => { - highpassText.Text = $"High Pass: {e.NewValue}hz"; - highpassFilter.Cutoff = e.NewValue; + highPassText.Text = $"High Pass: {e.NewValue}hz"; + highPassFilter.Cutoff = e.NewValue; }; } + #region Overrides of Drawable + + protected override void Update() + { + base.Update(); + highPassSlider.Current.Value = highPassFilter.Cutoff; + lowPassSlider.Current.Value = lowPassFilter.Cutoff; + } + + #endregion + [SetUpSteps] public void SetUpSteps() { AddStep("Play Track", () => track.Start()); + + AddStep("Reset filters", () => + { + lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF; + highPassFilter.Cutoff = 0; + }); + waitTrackPlay(); } [Test] - public void TestLowPass() + public void TestLowPassSweep() { AddStep("Filter Sweep", () => { - lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() .CutoffTo(0, 2000, Easing.OutCubic); }); @@ -103,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio AddStep("Filter Sweep (reverse)", () => { - lowpassFilter.CutoffTo(0).Then() + lowPassFilter.CutoffTo(0).Then() .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); }); @@ -112,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio } [Test] - public void TestHighPass() + public void TestHighPassSweep() { AddStep("Filter Sweep", () => { - highpassFilter.CutoffTo(0).Then() + highPassFilter.CutoffTo(0).Then() .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); }); @@ -124,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio AddStep("Filter Sweep (reverse)", () => { - highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() + highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() .CutoffTo(0, 2000, Easing.OutCubic); }); diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index 0152254945..d2a39e9db7 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -20,6 +20,8 @@ namespace osu.Game.Audio.Effects private readonly BQFParameters filter; private readonly BQFType type; + private bool isAttached; + private int cutoff; /// @@ -33,10 +35,8 @@ namespace osu.Game.Audio.Effects if (value == cutoff) return; - int oldValue = cutoff; cutoff = value; - - updateFilter(oldValue, cutoff); + updateFilter(cutoff); } } @@ -50,73 +50,58 @@ namespace osu.Game.Audio.Effects this.mixer = mixer; this.type = type; - switch (type) - { - case BQFType.HighPass: - cutoff = 1; - break; - - case BQFType.LowPass: - cutoff = MAX_LOWPASS_CUTOFF; - break; - - default: - cutoff = 500; // A default that should ensure audio remains audible for other filters. - break; - } - filter = new BQFParameters { lFilter = type, - fCenter = cutoff, fBandwidth = 0, - fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) + // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) + fQ = 0.7f }; - // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic) - if (type != BQFType.LowPass && type != BQFType.HighPass) - attachFilter(); + Cutoff = getInitialCutoff(type); } - private void attachFilter() + private int getInitialCutoff(BQFType type) { - Debug.Assert(!mixer.Effects.Contains(filter)); - mixer.Effects.Add(filter); - } - - private void detachFilter() - { - Debug.Assert(mixer.Effects.Contains(filter)); - mixer.Effects.Remove(filter); - } - - private void updateFilter(int oldValue, int newValue) - { - // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. - if (type == BQFType.LowPass) + switch (type) { - if (newValue >= MAX_LOWPASS_CUTOFF) - { - detachFilter(); - return; - } + case BQFType.HighPass: + return 1; - if (oldValue >= MAX_LOWPASS_CUTOFF) - attachFilter(); + case BQFType.LowPass: + return MAX_LOWPASS_CUTOFF; + + default: + return 500; // A default that should ensure audio remains audible for other filters. + } + } + + private void updateFilter(int newValue) + { + switch (type) + { + case BQFType.LowPass: + // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. + if (newValue >= MAX_LOWPASS_CUTOFF) + { + ensureDetached(); + return; + } + + break; + + // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. + case BQFType.HighPass: + if (newValue <= 1) + { + ensureDetached(); + return; + } + + break; } - // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. - if (type == BQFType.HighPass) - { - if (newValue <= 1) - { - detachFilter(); - return; - } - - if (oldValue <= 1) - attachFilter(); - } + ensureAttached(); var filterIndex = mixer.Effects.IndexOf(filter); @@ -131,12 +116,30 @@ namespace osu.Game.Audio.Effects } } + private void ensureAttached() + { + if (isAttached) + return; + + Debug.Assert(!mixer.Effects.Contains(filter)); + mixer.Effects.Add(filter); + isAttached = true; + } + + private void ensureDetached() + { + if (!isAttached) + return; + + Debug.Assert(mixer.Effects.Contains(filter)); + mixer.Effects.Remove(filter); + isAttached = false; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - - if (mixer.Effects.Contains(filter)) - detachFilter(); + ensureDetached(); } } } From e36c484060edeb764626b49a23b4ed45cf73ae11 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 15:30:58 +0900 Subject: [PATCH 19/29] Ensure rewinding before the spinner's start time --- .../TestSceneSpinnerRotation.cs | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 9da583a073..52ab39cfbd 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSpinnerRotation : TestSceneOsuPlayer { + private const double spinner_start_time = 100; + private const double spinner_duration = 6000; + [Resolved] private AudioManager audioManager { get; set; } @@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests double finalTrackerRotation = 0, trackerRotationTolerance = 0; double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0; - addSeekStep(5000); + addSeekStep(spinner_start_time + 5000); AddStep("retrieve disc rotation", () => { finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; @@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); - addSeekStep(2500); + addSeekStep(spinner_start_time + 2500); AddAssert("disc rotation rewound", // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // due to the exponential damping applied we're allowing a larger margin of error of about 10% @@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); - addSeekStep(5000); + addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); AddAssert("is symbol rotation almost same", @@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestSpinnerNormalBonusRewinding() { - addSeekStep(1000); + addSeekStep(spinner_start_time + 1000); AddAssert("player score matching expected bonus score", () => { @@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); } - private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay - { - Frames = scoreReplay - .Frames - .Cast() - .Select(replayFrame => - { - var adjustedTime = replayFrame.Time * rate; - return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray()); - }) - .Cast() - .ToList() - }; - private void addSeekStep(double time) { AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } @@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests new Spinner { Position = new Vector2(256, 192), - EndTime = 6000, + StartTime = spinner_start_time, + Duration = spinner_duration }, } }; From db5099de3ad38a6a986f4870ed30e4e97df51bfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 15:45:01 +0900 Subject: [PATCH 20/29] Add missing licence header --- osu.Game.Tests/Database/GeneralUsageTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 245981cd9b..3e8b6091fd 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -1,3 +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.Threading; using System.Threading.Tasks; From 93d7cdc509c8ec384c85e82228345ca778d429f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 15:50:05 +0900 Subject: [PATCH 21/29] Don't check whether the source realm was closed or not Based on what we now know, this is not required, as long as there is another realm context open on the same thread. --- osu.Game/Database/RealmLive.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 71fb44f617..abb69644d6 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -102,7 +102,7 @@ namespace osu.Game.Database } } - private bool originalDataValid => isCorrectThread && data.IsValid && !data.Realm.IsClosed; + private bool originalDataValid => isCorrectThread && data.IsValid; // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) private bool isCorrectThread From 5e934cdd2bdb4badf923c90fd8d66c0340f2f8a4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 17:42:55 +0900 Subject: [PATCH 22/29] Make CFS error and fail the job --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29cbdd2d37..128ae8f409 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,9 +79,12 @@ jobs: run: | # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. # FIXME: Suppress warnings from templates project - dotnet codefilesanity | while read -r line; do - echo "::warning::$line" - done + exit_code=0 + while read -r line; do + echo "::error::$line" + exit_code=1 + done <<< $(dotnet codefilesanity) + exit $exit_code # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded. # - name: .NET Format (Dry Run) From e12249f1270a22cf5811a8bb7a9ee44f2c0250db Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 17:56:33 +0900 Subject: [PATCH 23/29] Exclude empty lines --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 128ae8f409..0da1f9636b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,8 +81,10 @@ jobs: # FIXME: Suppress warnings from templates project exit_code=0 while read -r line; do + if [[ ! -z "$line" ]]; then echo "::error::$line" exit_code=1 + fi done <<< $(dotnet codefilesanity) exit $exit_code From 6d1b9be7fd4d5a482ac04dd9f01e0fe051bde2ff Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 17:56:56 +0900 Subject: [PATCH 24/29] Test CFS failure --- osu.Game/OsuGame.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 020cdebab6..1c277a3bd7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1,6 +1,3 @@ -// 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; From 08bbdc70fc96a26832911db0a133c46ca289d2cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 17:57:09 +0900 Subject: [PATCH 25/29] Revert "Test CFS failure" This reverts commit 6d1b9be7fd4d5a482ac04dd9f01e0fe051bde2ff. --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1c277a3bd7..020cdebab6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1,3 +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; using System.Diagnostics; From 2af1e6acc7bb0ffedda8fad659a215eed64ca3be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 18:02:22 +0900 Subject: [PATCH 26/29] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 956093b2ac..db62667fc2 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 184c9d3f63..c6121ddd5f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 38b920420b..110de79285 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From ceec81d54d8727986415a308686369453420b7ca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 20:26:54 +0900 Subject: [PATCH 27/29] Fix another spinner test failure --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs index 0ba775e5c7..37f1a846ad 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { new Spinner { - Duration = 2000, - Position = OsuPlayfield.BASE_SIZE / 2 + Duration = 6000, + Position = OsuPlayfield.BASE_SIZE / 2, } } }, From fdf714271117211dc4ed3897c307c2466203ad90 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 21:24:54 +0900 Subject: [PATCH 28/29] Remove Room.Position --- osu.Game/Online/Rooms/Room.cs | 8 -------- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 2 -- .../OnlinePlay/Lounge/Components/RoomsContainer.cs | 2 +- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 5f71b4be4a..39fc7f1da8 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -130,12 +130,6 @@ namespace osu.Game.Online.Rooms set => MaxAttempts.Value = value; } - /// - /// The position of this in the list. This is not read from or written to the API. - /// - [JsonIgnore] - public readonly Bindable Position = new Bindable(-1); // Todo: This does not need to exist. - public Room() { Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue)); @@ -192,8 +186,6 @@ namespace osu.Game.Online.Rooms RecentParticipants.Clear(); RecentParticipants.AddRange(other.RecentParticipants); } - - Position.Value = other.Position.Value; } public void RemoveExpiredPlaylistItems() diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 381849189d..97377278a6 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -116,8 +116,6 @@ namespace osu.Game.Screens.OnlinePlay.Components if (ignoredRooms.Contains(room.RoomID.Value.Value)) return; - room.Position.Value = -room.RoomID.Value.Value; - try { foreach (var pi in room.Playlist) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 907b7e308a..85efdcef1a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -129,7 +129,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void updateSorting() { foreach (var room in roomFlow) - roomFlow.SetLayoutPosition(room, room.Room.Position.Value); + roomFlow.SetLayoutPosition(room, -(room.Room.RoomID.Value ?? 0)); } protected override bool OnClick(ClickEvent e) From 47b6fb05f7cf0064f3cf900a048d142e4d4287da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Oct 2021 00:12:38 +0900 Subject: [PATCH 29/29] Add missing github sponsors funding spec --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0c6b80e97e..fc61573416 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: ppy custom: https://osu.ppy.sh/home/support