From 0633f3bcfe23513d671c0d5dc7bf7efcb603ba44 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 22 Nov 2021 16:35:58 +0900 Subject: [PATCH 001/335] Add owner id to playlist items --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + osu.Game/Online/Rooms/PlaylistItem.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index ad3c1f6781..df16fb3042 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -720,6 +720,7 @@ namespace osu.Game.Online.Multiplayer var playlistItem = new PlaylistItem { ID = item.ID, + OwnerID = item.OwnerID, Beatmap = { Value = beatmap }, Ruleset = { Value = ruleset }, Expired = item.Expired diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index c889dc514b..a1480865b8 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -18,6 +18,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("id")] public long ID { get; set; } + [JsonProperty("owner_id")] + public int OwnerID { get; set; } + [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } From d2062ff97fda0507ca1a1c12f853eb6713f548c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 15:23:16 +0900 Subject: [PATCH 002/335] Reformat realm migrations list for legibility --- osu.Game/Database/RealmContextFactory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0bb46a379a..e773ea9767 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -33,10 +33,10 @@ namespace osu.Game.Database /// /// Version history: - /// 6 First tracked version (~20211018) - /// 7 Changed OnlineID fields to non-nullable to add indexing support (20211018) - /// 8 Rebind scroll adjust keys to not have control modifier (20211029) - /// 9 Converted BeatmapMetadata.Author from string to RealmUser (20211104) + /// 6 ~2021-10-18 First tracked version. + /// 7 2021-10-18 Changed OnlineID fields to non-nullable to add indexing support. + /// 8 2021-10-29 Rebind scroll adjust keys to not have control modifier. + /// 9 2021-11-04 Converted BeatmapMetadata.Author from string to RealmUser. /// private const int schema_version = 9; From ca26b6c5408309256ebbc4d89324cc52d472823a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 18:51:37 +0900 Subject: [PATCH 003/335] Provide `RealmContextFactory` with the EF `RulesetStore` for migration purposes --- osu.Game/Database/RealmContextFactory.cs | 6 +++++- osu.Game/OsuGameBase.cs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index e773ea9767..1d0da3ea5c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -13,6 +13,7 @@ using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Input.Bindings; using osu.Game.Models; +using osu.Game.Rulesets; using Realms; #nullable enable @@ -31,6 +32,8 @@ namespace osu.Game.Database /// public readonly string Filename; + private readonly RulesetStore? rulesets; + /// /// Version history: /// 6 ~2021-10-18 First tracked version. @@ -72,9 +75,10 @@ namespace osu.Game.Database } } - public RealmContextFactory(Storage storage, string filename) + public RealmContextFactory(Storage storage, string filename, RulesetStore? rulesets = null) { this.storage = storage; + this.rulesets = rulesets; Filename = filename; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e207d9ce3b..3b2f397a72 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -187,8 +187,9 @@ namespace osu.Game Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", RulesetStore)); dependencies.CacheAs(Storage); @@ -227,7 +228,6 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() From 329bae50b0e99b3450fc84daa9f3a4828c433c11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 18:07:28 +0900 Subject: [PATCH 004/335] Switch realm ruleset configuration to use ruleset's `ShortName` as key --- osu.Game/Configuration/RealmRulesetSetting.cs | 9 ++----- osu.Game/Database/RealmContextFactory.cs | 26 ++++++++++++++++++- osu.Game/OsuGameBase.cs | 3 ++- .../Configuration/RulesetConfigManager.cs | 13 ++++------ 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/osu.Game/Configuration/RealmRulesetSetting.cs b/osu.Game/Configuration/RealmRulesetSetting.cs index 07e56ad8dd..3fea35ee9d 100644 --- a/osu.Game/Configuration/RealmRulesetSetting.cs +++ b/osu.Game/Configuration/RealmRulesetSetting.cs @@ -1,8 +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 osu.Game.Database; using Realms; #nullable enable @@ -10,13 +8,10 @@ using Realms; namespace osu.Game.Configuration { [MapTo(@"RulesetSetting")] - public class RealmRulesetSetting : RealmObject, IHasGuidPrimaryKey + public class RealmRulesetSetting : RealmObject { - [PrimaryKey] - public Guid ID { get; set; } = Guid.NewGuid(); - [Indexed] - public int RulesetID { get; set; } + public string RulesetName { get; set; } = string.Empty; [Indexed] public int Variant { get; set; } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 1d0da3ea5c..70ea24b581 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; using osu.Game.Rulesets; @@ -40,8 +41,9 @@ namespace osu.Game.Database /// 7 2021-10-18 Changed OnlineID fields to non-nullable to add indexing support. /// 8 2021-10-29 Rebind scroll adjust keys to not have control modifier. /// 9 2021-11-04 Converted BeatmapMetadata.Author from string to RealmUser. + /// 10 2021-11-22 Use ShortName instead of RulesetID for ruleset settings. /// - private const int schema_version = 9; + private const int schema_version = 10; /// /// Lock object which is held during sections, blocking context creation during blocking periods. @@ -236,6 +238,28 @@ namespace osu.Game.Database }; } + break; + + case 10: + string rulesetSettingClassName = getMappedOrOriginalName(typeof(RealmRulesetSetting)); + + var oldSettings = migration.OldRealm.DynamicApi.All(rulesetSettingClassName); + var newSettings = migration.NewRealm.All().ToList(); + + for (int i = 0; i < newSettings.Count; i++) + { + dynamic? oldItem = oldSettings.ElementAt(i); + var newItem = newSettings.ElementAt(i); + + long rulesetId = oldItem.RulesetID; + string? rulesetName = rulesets?.GetRuleset((int)rulesetId)?.ShortName; + + if (string.IsNullOrEmpty(rulesetName)) + migration.NewRealm.Remove(newItem); + else + newItem.RulesetName = rulesetName; + } + break; } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3b2f397a72..d8050353c5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -456,7 +456,8 @@ namespace osu.Game { Key = dkb.Key, Value = dkb.StringValue, - RulesetID = dkb.RulesetID.Value, + // important: this RulesetStore must be the EF one. + RulesetName = RulesetStore.GetRuleset(dkb.RulesetID.Value).ShortName, Variant = dkb.Variant ?? 0, }); } diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index eec71a3623..17678775e9 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -20,16 +20,13 @@ namespace osu.Game.Rulesets.Configuration private List databasedSettings = new List(); - private readonly int rulesetId; + private readonly string rulesetName; protected RulesetConfigManager(SettingsStore store, RulesetInfo ruleset, int? variant = null) { realmFactory = store?.Realm; - if (realmFactory != null && !ruleset.ID.HasValue) - throw new InvalidOperationException("Attempted to add databased settings for a non-databased ruleset"); - - rulesetId = ruleset.ID ?? -1; + rulesetName = ruleset.ShortName; this.variant = variant ?? 0; @@ -43,7 +40,7 @@ namespace osu.Game.Rulesets.Configuration if (realmFactory != null) { // As long as RulesetConfigCache exists, there is no need to subscribe to realm events. - databasedSettings = realmFactory.Context.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + databasedSettings = realmFactory.Context.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).ToList(); } } @@ -68,7 +65,7 @@ namespace osu.Game.Rulesets.Configuration { foreach (var c in changed) { - var setting = realm.All().First(s => s.RulesetID == rulesetId && s.Variant == variant && s.Key == c.ToString()); + var setting = realm.All().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); setting.Value = ConfigStore[c].ToString(); } @@ -94,7 +91,7 @@ namespace osu.Game.Rulesets.Configuration { Key = lookup.ToString(), Value = bindable.Value.ToString(), - RulesetID = rulesetId, + RulesetName = rulesetName, Variant = variant, }; From d94b27a8a22bae840d5a1eaac09468946ac8dec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Nov 2021 18:34:04 +0900 Subject: [PATCH 005/335] Switch realm ruleset key bindings to use ruleset's `ShortName` as key --- osu.Game/Database/RealmContextFactory.cs | 30 ++++++++++++++++++- .../Bindings/DatabasedKeyBindingContainer.cs | 25 +++++++--------- osu.Game/Input/Bindings/RealmKeyBinding.cs | 6 ++-- osu.Game/Input/RealmKeyBindingStore.cs | 10 +++---- .../Settings/Sections/Input/KeyBindingRow.cs | 2 +- .../Sections/Input/KeyBindingsSubsection.cs | 4 +-- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- 7 files changed, 53 insertions(+), 26 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 70ea24b581..2a2c94ee7d 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -42,8 +42,9 @@ namespace osu.Game.Database /// 8 2021-10-29 Rebind scroll adjust keys to not have control modifier. /// 9 2021-11-04 Converted BeatmapMetadata.Author from string to RealmUser. /// 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. /// - private const int schema_version = 10; + private const int schema_version = 11; /// /// Lock object which is held during sections, blocking context creation during blocking periods. @@ -260,6 +261,33 @@ namespace osu.Game.Database newItem.RulesetName = rulesetName; } + break; + + case 11: + + string keyBindingClassName = getMappedOrOriginalName(typeof(RealmKeyBinding)); + + var oldKeyBindings = migration.OldRealm.DynamicApi.All(keyBindingClassName); + var newKeyBindings = migration.NewRealm.All().ToList(); + + for (int i = 0; i < newKeyBindings.Count; i++) + { + dynamic? oldItem = oldKeyBindings.ElementAt(i); + var newItem = newKeyBindings.ElementAt(i); + + if (oldItem.RulesetID == null) + continue; + + long rulesetId = oldItem.RulesetID; + + string? rulesetName = rulesets?.GetRuleset((int)rulesetId)?.ShortName; + + if (string.IsNullOrEmpty(rulesetName)) + migration.NewRealm.Remove(newItem); + else + newItem.RulesetName = rulesetName; + } + break; } } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 5dced23614..baa5b9ff9c 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -50,23 +50,20 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - if (ruleset == null || ruleset.ID.HasValue) - { - int? rulesetId = ruleset?.ID; + string rulesetName = ruleset?.ShortName; - realmKeyBindings = realmFactory.Context.All() - .Where(b => b.RulesetID == rulesetId && b.Variant == variant); + realmKeyBindings = realmFactory.Context.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant); - realmSubscription = realmKeyBindings - .SubscribeForNotifications((sender, changes, error) => - { - // first subscription ignored as we are handling this in LoadComplete. - if (changes == null) - return; + realmSubscription = realmKeyBindings + .SubscribeForNotifications((sender, changes, error) => + { + // first subscription ignored as we are handling this in LoadComplete. + if (changes == null) + return; - ReloadMappings(); - }); - } + ReloadMappings(); + }); base.LoadComplete(); } diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 334d2da427..6a408847fe 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -6,6 +6,8 @@ using osu.Framework.Input.Bindings; using osu.Game.Database; using Realms; +#nullable enable + namespace osu.Game.Input.Bindings { [MapTo(nameof(KeyBinding))] @@ -14,7 +16,7 @@ namespace osu.Game.Input.Bindings [PrimaryKey] public Guid ID { get; set; } = Guid.NewGuid(); - public int? RulesetID { get; set; } + public string? RulesetName { get; set; } public int? Variant { get; set; } @@ -34,6 +36,6 @@ namespace osu.Game.Input.Bindings public int ActionInt { get; set; } [MapTo(nameof(KeyCombination))] - public string KeyCombinationString { get; set; } + public string KeyCombinationString { get; set; } = string.Empty; } } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 046969579c..3bdb0a180d 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -36,7 +36,7 @@ namespace osu.Game.Input using (var context = realmFactory.CreateContext()) { - foreach (var action in context.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) + foreach (var action in context.All().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction)) { string str = keyCombinationProvider.GetReadableString(action.KeyCombination); @@ -69,20 +69,20 @@ namespace osu.Game.Input { var instance = ruleset.CreateInstance(); foreach (int variant in instance.AvailableVariants) - insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); + insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant); } transaction.Commit(); } } - private void insertDefaults(Realm realm, List existingBindings, IEnumerable defaults, int? rulesetId = null, int? variant = null) + private void insertDefaults(Realm realm, List existingBindings, IEnumerable defaults, string? rulesetName = null, int? variant = null) { // compare counts in database vs defaults for each action type. foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) { // avoid performing redundant queries when the database is empty and needs to be re-filled. - int existingCount = existingBindings.Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key); + int existingCount = existingBindings.Count(k => k.RulesetName == rulesetName && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key); if (defaultsForAction.Count() <= existingCount) continue; @@ -92,7 +92,7 @@ namespace osu.Game.Input { KeyCombinationString = k.KeyCombination.ToString(), ActionInt = (int)k.Action, - RulesetID = rulesetId, + RulesetName = rulesetName, Variant = variant })); } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 96a685a9c5..e0a1a82326 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -524,7 +524,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input public void UpdateKeyCombination(KeyCombination newCombination) { - if (KeyBinding.RulesetID != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) + if (KeyBinding.RulesetName != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination)) return; KeyBinding.KeyCombination = newCombination; diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index be0830a7c2..115a7bdc79 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -32,12 +32,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input [BackgroundDependencyLoader] private void load(RealmContextFactory realmFactory) { - int? rulesetId = Ruleset?.ID; + string rulesetName = Ruleset?.ShortName; List bindings; using (var realm = realmFactory.CreateContext()) - bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + bindings = realm.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).Detach(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index b2252a5575..75bebfa763 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -207,7 +207,7 @@ namespace osu.Game.Overlays.Toolbar { if (Hotkey == null) return; - var realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); + var realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value); if (realmKeyBinding != null) { From 2350806b4cc8fbe23ee21288e5c75d16407133a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Nov 2021 20:26:45 +0100 Subject: [PATCH 006/335] Add failing test case for number box stack overflow scenario --- .../Settings/TestSceneSettingsNumberBox.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneSettingsNumberBox.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsNumberBox.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsNumberBox.cs new file mode 100644 index 0000000000..ffa1200f32 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsNumberBox.cs @@ -0,0 +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.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Tests.Visual.Settings +{ + public class TestSceneSettingsNumberBox : OsuTestScene + { + [Test] + public void TestLargeInteger() + { + SettingsNumberBox numberBox = null; + + AddStep("create number box", () => Child = numberBox = new SettingsNumberBox()); + + AddStep("set value to 1,000,000,000", () => numberBox.Current.Value = 1_000_000_000); + AddAssert("text box text is correct", () => numberBox.ChildrenOfType().Single().Current.Value == "1000000000"); + } + } +} From dced6a2e682a9902f43e1901e2ae1912bb637016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Nov 2021 20:39:44 +0100 Subject: [PATCH 007/335] Add extended test coverage for desired input handling --- .../Settings/TestSceneSettingsNumberBox.cs | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsNumberBox.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsNumberBox.cs index ffa1200f32..c063e5526a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsNumberBox.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsNumberBox.cs @@ -11,15 +11,68 @@ namespace osu.Game.Tests.Visual.Settings { public class TestSceneSettingsNumberBox : OsuTestScene { + private SettingsNumberBox numberBox; + private OsuTextBox textBox; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create number box", () => Child = numberBox = new SettingsNumberBox()); + AddStep("get inner text box", () => textBox = numberBox.ChildrenOfType().Single()); + } + [Test] public void TestLargeInteger() { - SettingsNumberBox numberBox = null; - - AddStep("create number box", () => Child = numberBox = new SettingsNumberBox()); - - AddStep("set value to 1,000,000,000", () => numberBox.Current.Value = 1_000_000_000); - AddAssert("text box text is correct", () => numberBox.ChildrenOfType().Single().Current.Value == "1000000000"); + AddStep("set current to 1,000,000,000", () => numberBox.Current.Value = 1_000_000_000); + AddAssert("text box text is correct", () => textBox.Text == "1000000000"); } + + [Test] + public void TestUserInput() + { + inputText("42"); + currentValueIs(42); + currentTextIs("42"); + + inputText(string.Empty); + currentValueIs(null); + currentTextIs(string.Empty); + + inputText("555"); + currentValueIs(555); + currentTextIs("555"); + + inputText("-4444"); + // attempting to input the minus will raise an input error, the rest will pass through fine. + currentValueIs(4444); + currentTextIs("4444"); + + // checking the upper bound. + inputText(int.MaxValue.ToString()); + currentValueIs(int.MaxValue); + currentTextIs(int.MaxValue.ToString()); + + inputText((long)int.MaxValue + 1.ToString()); + currentValueIs(int.MaxValue); + currentTextIs(int.MaxValue.ToString()); + + inputText("0"); + currentValueIs(0); + currentTextIs("0"); + + // checking that leading zeroes are stripped. + inputText("00"); + currentValueIs(0); + currentTextIs("0"); + + inputText("01"); + currentValueIs(1); + currentTextIs("1"); + } + + private void inputText(string text) => AddStep($"set textbox text to {text}", () => textBox.Text = text); + private void currentValueIs(int? value) => AddAssert($"current value is {value?.ToString() ?? "null"}", () => numberBox.Current.Value == value); + private void currentTextIs(string value) => AddAssert($"current text is {value}", () => textBox.Text == value); } } From 4a9f080f3c0c227ff17ab5b4dc1744d4c433e354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Nov 2021 20:41:27 +0100 Subject: [PATCH 008/335] Accept full range of `int` in `SettingsNumberBox` This fixes stack overflow exceptions that would arise when a `Current.Value` of 1 billion or more was set on a `SettingsNumberBox`. The stack overflow was caused by the "maximum 9 digits" spec. If a value technically within `int` bounds, but larger than 1 billion (in the range [1,000,000,000; 2,147,483,647], to be more precise), a feedback loop between the setting control's `Current` and its inner text box's `Current` would occur, wherein the last digit would be trimmed and then re-appended again forevermore. To resolve, remove the offending spec and rely on `int.TryParse` entirely to be able to discern overflow range. Additionally, UX of the text box is slightly changed to notify when the `int` range is exceeded with a red flash. This behaviour would not have been possible to implement without recent framework-side fixes to text box (removal of text set scheduling). --- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index 545f1050b2..cbe9f7fc64 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -35,7 +35,6 @@ namespace osu.Game.Overlays.Settings { numberBox = new OutlinedNumberBox { - LengthLimit = 9, // limited to less than a value that could overflow int32 backing. Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, CommitOnFocusLost = true @@ -44,12 +43,19 @@ namespace osu.Game.Overlays.Settings numberBox.Current.BindValueChanged(e => { - int? value = null; + if (string.IsNullOrEmpty(e.NewValue)) + { + Current.Value = null; + return; + } if (int.TryParse(e.NewValue, out int intVal)) - value = intVal; + Current.Value = intVal; + else + numberBox.NotifyInputError(); - current.Value = value; + // trigger Current again to either restore the previous text box value, or to reformat the new value via .ToString(). + Current.TriggerChange(); }); Current.BindValueChanged(e => @@ -62,6 +68,8 @@ namespace osu.Game.Overlays.Settings private class OutlinedNumberBox : OutlinedTextBox { protected override bool CanAddCharacter(char character) => char.IsNumber(character); + + public new void NotifyInputError() => base.NotifyInputError(); } } } From 1f321e29107559f3e099ea9acf9f79988be94dd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Nov 2021 11:48:56 +0900 Subject: [PATCH 009/335] Run EF migrations earlier to ensure it is complete before usage --- osu.Game/OsuGameBase.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d8050353c5..a2dd417491 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -187,6 +187,9 @@ namespace osu.Game Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + + runMigrations(); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", RulesetStore)); @@ -204,8 +207,6 @@ namespace osu.Game Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; - runMigrations(); - dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Resources, Audio)); dependencies.CacheAs(SkinManager); From 10bd7176e015f63e4bf2dfa829db66ce6d4a71f4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Nov 2021 14:06:06 +0900 Subject: [PATCH 010/335] Fix potential test failure in TestSceneMultiplayerReadyButton Couldn't exactly reproduce https://github.com/ppy/osu/runs/4294316800, but I found a similar issue via: ```diff diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 84b63a5733..29cac9b061 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; @@ -81,6 +82,8 @@ private void load(GameHost host, AudioManager audio) await Client.ToggleReady(); + Thread.Sleep(1000); + readyClickOperation.Dispose(); }); } ``` --- .../Multiplayer/TestSceneMultiplayerReadyButton.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index e9612bf55c..84b63a5733 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; @@ -198,11 +199,15 @@ namespace osu.Game.Tests.Visual.Multiplayer }, users); } - private void addClickButtonStep() => AddStep("click button", () => + private void addClickButtonStep() { - InputManager.MoveMouseTo(button); - InputManager.Click(MouseButton.Left); - }); + AddUntilStep("wait for button to be ready", () => button.ChildrenOfType public class TestMultiplayerRoomManager : MultiplayerRoomManager { + public bool RoomJoined { get; private set; } + private readonly TestRoomRequestsHandler requestsHandler; public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler) @@ -24,6 +27,30 @@ namespace osu.Game.Tests.Visual.Multiplayer public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms; + public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + { + base.CreateRoom(room, r => + { + onSuccess?.Invoke(r); + RoomJoined = true; + }, onError); + } + + public override void JoinRoom(Room room, string password = null, Action onSuccess = null, Action onError = null) + { + base.JoinRoom(room, password, r => + { + onSuccess?.Invoke(r); + RoomJoined = true; + }, onError); + } + + public override void PartRoom() + { + base.PartRoom(); + RoomJoined = false; + } + /// /// Adds a room to a local "server-side" list that's returned when a is fired. /// From 1f136696362aab0824fb85e6e90693c85c5820ef Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Nov 2021 16:13:53 +0900 Subject: [PATCH 015/335] Don't poll while in room Fixes timeout in `TestJoinRoomWithoutPassword`, because the 'server' returns out-of-date data while the `MatchSubScreen` has possible not been entered yet (and thus hasn't disabled polling itself yet). Can be tested by adding a `Task.Delay(3000);` at the end of the `MultiplayerClient.JoinRoom()` task. --- .../OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index cf1066df10..5cdec52bc2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -93,6 +93,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!isConnected.Value) return Task.CompletedTask; + if (client.Room != null) + return Task.CompletedTask; + return base.Poll(); } } From 6363833fb3c1071bcea9a8d021270bf0cc64056b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Nov 2021 16:16:38 +0900 Subject: [PATCH 016/335] Revert unnecessary changes --- osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs | 2 -- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs index 43cb857c9a..7f1171db1f 100644 --- a/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs +++ b/osu.Game.Tests/Visual/TestMultiplayerScreenStack.cs @@ -7,7 +7,6 @@ using osu.Framework.Screens; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Screens; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.OnlinePlay; @@ -77,7 +76,6 @@ namespace osu.Game.Tests.Visual { public new TestMultiplayerRoomManager RoomManager { get; private set; } public TestRoomRequestsHandler RequestsHandler { get; private set; } - public new OngoingOperationTracker OngoingOperationTracker => base.OngoingOperationTracker; protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager(RequestsHandler = new TestRoomRequestsHandler()); } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 6df79df535..a18e4b45cf 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay protected RoomManager RoomManager { get; private set; } [Cached] - protected readonly OngoingOperationTracker OngoingOperationTracker = new OngoingOperationTracker(); + private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); [Resolved(CanBeNull = true)] private MusicController music { get; set; } @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }, new Header(ScreenTitle, screenStack), RoomManager, - OngoingOperationTracker + ongoingOperationTracker } }; } From 0cf5a738dc9a0851e74d546ad155001f0e8ca646 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Nov 2021 16:32:44 +0900 Subject: [PATCH 017/335] Remove unused using --- osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs index 22d9a4ff9d..db89855db7 100644 --- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs @@ -3,7 +3,6 @@ using System.IO; using NUnit.Framework; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Game.Tournament.IO; From a521d1b83584c40f8ed201716c0b83e3b4ffa8b1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Nov 2021 17:09:38 +0900 Subject: [PATCH 018/335] Wait for ready button to be enabled first --- .../Visual/Multiplayer/QueueModeTestScene.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 8e731e34ea..357db16e2c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -104,23 +105,24 @@ namespace osu.Game.Tests.Visual.Multiplayer protected void RunGameplay() { AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); - - AddStep("click ready button", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); + clickReadyButton(); AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); - - AddStep("click ready button", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); + clickReadyButton(); AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player player && player.IsLoaded); AddStep("exit player", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); } + + private void clickReadyButton() + { + AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType().Single().ChildrenOfType