From 5a953f38119cdca4a3a6a214110b2d2f934ab013 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Dec 2021 19:14:33 +0900 Subject: [PATCH 01/15] Fix autopilot not working as expected on touch devices Closes https://github.com/ppy/osu/issues/12731. I haven't tested this, but quite confident it should work. Will test later today unless someone else beats me. --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index 7314021a14..f5fc3de381 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges.Events; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu @@ -39,6 +40,17 @@ namespace osu.Game.Rulesets.Osu return base.Handle(e); } + protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e) + { + if (!AllowUserCursorMovement) + { + // Still allow for forwarding of the "touch" part, but block the positional data. + e = new TouchStateChangeEvent(e.State, e.Input, e.Touch, false, null); + } + + return base.HandleMouseTouchStateChange(e); + } + private class OsuKeyBindingContainer : RulesetKeyBindingContainer { public bool AllowUserPresses = true; From bc1f1f35b5a73dde39bfaab211300df330f5f7f3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Dec 2021 16:40:51 +0300 Subject: [PATCH 02/15] Remove now redundant inclusion of `TouchMoveEvent` in `OsuInputManager.Handle` Now it's handled separately via the `HandleMouseTouchStateChange` override. --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index f5fc3de381..57704b3bd8 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu protected override bool Handle(UIEvent e) { - if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false; + if (e is MouseMoveEvent && !AllowUserCursorMovement) return false; return base.Handle(e); } From cf3041128888b4be5993728e8604c90df1cdd49b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Dec 2021 17:13:13 +0300 Subject: [PATCH 03/15] Revert "Remove now redundant inclusion of `TouchMoveEvent` in `OsuInputManager.Handle`" This reverts commit bc1f1f35b5a73dde39bfaab211300df330f5f7f3. --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index 57704b3bd8..f5fc3de381 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu protected override bool Handle(UIEvent e) { - if (e is MouseMoveEvent && !AllowUserCursorMovement) return false; + if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false; return base.Handle(e); } From 8e6c7eb030fa523a6fe64273e4480b24fa6e49bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 13:52:27 +0900 Subject: [PATCH 04/15] Use `OsuStorage` in realm tests to allow for migration Also changes the realm filename to use `client` to match the ignore rules in `OsuStorage`. Without doing this, migration will fail in an indefinite mutex wait when attempting to delete the realm `.note` file. --- osu.Game.Tests/Database/RealmTest.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 04c9f2577a..6904464485 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -10,6 +10,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.IO; using osu.Game.Models; #nullable enable @@ -27,15 +28,16 @@ namespace osu.Game.Tests.Database storage.DeleteDirectory(string.Empty); } - protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") + protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") { using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller)) { host.Run(new RealmTestGame(() => { - var testStorage = storage.GetStorageForDirectory(caller); + // ReSharper disable once AccessToDisposedClosure + var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller)); - using (var realmFactory = new RealmContextFactory(testStorage, caller)) + using (var realmFactory = new RealmContextFactory(testStorage, "client")) { Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); testAction(realmFactory, testStorage); @@ -58,7 +60,7 @@ namespace osu.Game.Tests.Database { var testStorage = storage.GetStorageForDirectory(caller); - using (var realmFactory = new RealmContextFactory(testStorage, caller)) + using (var realmFactory = new RealmContextFactory(testStorage, "client")) { Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); await testAction(realmFactory, testStorage); From be337b4acea61c1bfea6b05442ad5d0201d63d3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 13:53:23 +0900 Subject: [PATCH 05/15] Add failing test coverage of `RealmLive` failing post storage migration --- osu.Game.Tests/Database/RealmLiveTests.cs | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 9b6769b788..1d197791a4 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.Framework.Testing; using osu.Game.Database; using osu.Game.Models; using Realms; @@ -29,6 +30,33 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestAccessAfterStorageMigrate() + { + RunTestWithRealm((realmFactory, storage) => + { + var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); + + ILive liveBeatmap; + + using (var context = realmFactory.CreateContext()) + { + context.Write(r => r.Add(beatmap)); + + liveBeatmap = beatmap.ToLive(); + } + + using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) + { + migratedStorage.DeleteDirectory(string.Empty); + + storage.Migrate(migratedStorage); + + Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); + } + }); + } + [Test] public void TestAccessAfterAttach() { From f9a2db5ec6dbf7e87b0cf2c578d7f81be0aa0138 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 14:19:43 +0900 Subject: [PATCH 06/15] Add accessibility to realm factory via `IStorageResourceProvider` We might need to rename this class.. --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 2 ++ osu.Game/Beatmaps/WorkingBeatmapCache.cs | 2 ++ osu.Game/IO/IStorageResourceProvider.cs | 6 ++++++ osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 2 ++ osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 ++ 5 files changed, 14 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 88f35976ad..3aab28886e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -15,6 +15,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Audio; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -220,6 +221,7 @@ namespace osu.Game.Tests.Gameplay public AudioManager AudioManager => Audio; public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; + public RealmContextFactory RealmContextFactory => null; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; #endregion diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 559685d3c4..449406eadf 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -15,6 +15,7 @@ using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -108,6 +109,7 @@ namespace osu.Game.Beatmaps TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; AudioManager IStorageResourceProvider.AudioManager => audioManager; + RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; IResourceStore IStorageResourceProvider.Files => files; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs index e4c97e18fa..950b5aae09 100644 --- a/osu.Game/IO/IStorageResourceProvider.cs +++ b/osu.Game/IO/IStorageResourceProvider.cs @@ -4,6 +4,7 @@ using osu.Framework.Audio; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Game.Database; namespace osu.Game.IO { @@ -24,6 +25,11 @@ namespace osu.Game.IO /// IResourceStore Resources { get; } + /// + /// Access realm. + /// + RealmContextFactory RealmContextFactory { get; } + /// /// Create a texture loader store based on an underlying data store. /// diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 31a2071249..f919edecf7 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -14,6 +14,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Models; using osu.Game.Rulesets; @@ -118,6 +119,7 @@ namespace osu.Game.Tests.Beatmaps public IResourceStore Files => userSkinResourceStore; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; + RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; #endregion diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index cdd3e47930..22aac79056 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.IO; using osu.Game.Rulesets; @@ -158,6 +159,7 @@ namespace osu.Game.Tests.Visual public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; #endregion From 441b7baa93225658fd82ed78420fd597bcd8bb62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Dec 2021 14:21:23 +0900 Subject: [PATCH 07/15] Provide a realm factory to usages of `ToLive`/`RealmLive` --- .../Editor/TestSceneManiaComposeScreen.cs | 4 +-- osu.Game.Tests/Database/RealmLiveTests.cs | 22 +++++++------- .../TestSceneBackgroundScreenDefault.cs | 2 +- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 2 +- osu.Game/Database/RealmLive.cs | 30 ++++++++++++------- osu.Game/Database/RealmObjectExtensions.cs | 20 ++++++++++--- osu.Game/OsuGame.cs | 4 +-- .../Overlays/Settings/Sections/SkinSection.cs | 6 ++-- osu.Game/Skinning/Skin.cs | 2 +- osu.Game/Skinning/SkinManager.cs | 13 ++++---- osu.Game/Stores/RealmArchiveModelImporter.cs | 6 ++-- 11 files changed, 67 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs index 91f5f93905..a30e09cd29 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs @@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestDefaultSkin() { - AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLive()); + AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged()); } [Test] public void TestLegacySkin() { - AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLive()); + AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged()); } } } diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 1d197791a4..06cb5a3607 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -22,9 +22,9 @@ namespace osu.Game.Tests.Database { RunTestWithRealm((realmFactory, _) => { - ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(); + ILive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(realmFactory); - ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(); + ILive beatmap2 = realmFactory.CreateContext().All().First().ToLive(realmFactory); Assert.AreEqual(beatmap, beatmap2); }); @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Database { context.Write(r => r.Add(beatmap)); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target")) @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Database { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLive(realmFactory); using (var context = realmFactory.CreateContext()) context.Write(r => r.Add(beatmap)); @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Database public void TestAccessNonManaged() { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLiveUnmanaged(); Assert.IsFalse(beatmap.Hidden); Assert.IsFalse(liveBeatmap.Value.Hidden); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Database RunTestWithRealm((realmFactory, _) => { var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(); + var liveBeatmap = beatmap.ToLive(realmFactory); Assert.DoesNotThrow(() => { @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Database { var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Database // not just a refresh from the resolved Live. threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(); + liveBeatmap = beatmap.ToLive(realmFactory); } }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index bdd1b92c8d..4762d3cded 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Background private void setCustomSkin() { // feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin. - AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLive()); + AddStep("set custom skin", () => skins.CurrentSkinInfo.Value = new SkinInfo().ToLiveUnmanaged()); } private void setDefaultSkin() => AddStep("set default skin", () => skins.CurrentSkinInfo.SetDefault()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index cccc962a3f..c5f56cae9e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("setup skins", () => { - skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLive(); + skinManager.CurrentSkinInfo.Value = gameCurrentSkin.ToLiveUnmanaged(); currentBeatmapSkin = getBeatmapSkin(); }); }); diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index c376d5d503..4f7bdf93e4 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -24,14 +24,22 @@ namespace osu.Game.Database /// private readonly T data; + private readonly RealmContextFactory? realmFactory; + /// /// Construct a new instance of live realm data. /// /// The realm data. - public RealmLive(T data) + /// The realm factory the data was sourced from. May be null for an unmanaged object. + public RealmLive(T data, RealmContextFactory? realmFactory) { this.data = data; + if (IsManaged && realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + this.realmFactory = realmFactory; + ID = data.ID; } @@ -47,7 +55,10 @@ namespace osu.Game.Database return; } - using (var realm = Realm.GetInstance(data.Realm.Config)) + if (realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + using (var realm = realmFactory.CreateContext()) perform(realm.Find(ID)); } @@ -58,12 +69,15 @@ namespace osu.Game.Database 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)}."); + throw new InvalidOperationException(@$"Realm live objects should not exit the scope of {nameof(PerformRead)}."); if (!IsManaged) return perform(data); - using (var realm = Realm.GetInstance(data.Realm.Config)) + if (realmFactory == null) + throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); + + using (var realm = realmFactory.CreateContext()) return perform(realm.Find(ID)); } @@ -74,7 +88,7 @@ namespace osu.Game.Database public void PerformWrite(Action perform) { if (!IsManaged) - throw new InvalidOperationException("Can't perform writes on a non-managed underlying value"); + throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); PerformRead(t => { @@ -94,11 +108,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); - // When using Value, we rely on garbage collection for the realm instance used to retrieve the instance. - // As we are sure that this is on the update thread, there should always be an open and constantly refreshing realm instance to ensure file size growth is a non-issue. - var realm = Realm.GetInstance(data.Realm.Config); - - return realm.Find(ID); + return realmFactory!.Context.Find(ID); } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index b38e21453c..c546a70fae 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -53,16 +53,28 @@ namespace osu.Game.Database return mapper.Map(item); } - public static List> ToLive(this IEnumerable realmList) + public static List> ToLiveUnmanaged(this IEnumerable realmList) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLive(l)).Cast>().ToList(); + return realmList.Select(l => new RealmLive(l, null)).Cast>().ToList(); } - public static ILive ToLive(this T realmObject) + public static ILive ToLiveUnmanaged(this T realmObject) where T : RealmObject, IHasGuidPrimaryKey { - return new RealmLive(realmObject); + return new RealmLive(realmObject, null); + } + + public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory) + where T : RealmObject, IHasGuidPrimaryKey + { + return realmList.Select(l => new RealmLive(l, realmContextFactory)).Cast>().ToList(); + } + + public static ILive ToLive(this T realmObject, RealmContextFactory realmContextFactory) + where T : RealmObject, IHasGuidPrimaryKey + { + return new RealmLive(realmObject, realmContextFactory); } /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a35191613c..9c379de683 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -255,10 +255,10 @@ namespace osu.Game if (skinInfo == null) { if (guid == SkinInfo.CLASSIC_SKIN) - skinInfo = DefaultLegacySkin.CreateInfo().ToLive(); + skinInfo = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged(); } - SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLive(); + SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLiveUnmanaged(); }; configSkin.TriggerChange(); diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index b1582d7bee..0fa6d78d4b 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -32,14 +32,14 @@ namespace osu.Game.Overlays.Settings.Sections Icon = FontAwesome.Solid.PaintBrush }; - private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLive() }; + private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() }; private readonly Bindable configBindable = new Bindable(); private static readonly ILive random_skin_info = new SkinInfo { ID = SkinInfo.RANDOM_SKIN, Name = "", - }.ToLive(); + }.ToLiveUnmanaged(); private List> skinItems; @@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Settings.Sections { int protectedCount = realmSkins.Count(s => s.Protected); - skinItems = realmSkins.ToLive(); + skinItems = realmSkins.ToLive(realmFactory); skinItems.Insert(protectedCount, random_skin_info); diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 54fc2340f1..ee92b6b40a 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,7 +43,7 @@ namespace osu.Game.Skinning protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = skin.ToLive(); + SkinInfo = skin.ToLive(resources.RealmContextFactory); this.resources = resources; configurationStream ??= getConfigurationStream(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 5134632fb1..bb2f0a37b4 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -47,9 +47,9 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(); - public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLive()) + public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()) { - Default = Skinning.DefaultSkin.CreateInfo().ToLive() + Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged() }; private readonly SkinModelManager skinModelManager; @@ -119,13 +119,13 @@ namespace osu.Game.Skinning if (randomChoices.Length == 0) { - CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive(); + CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged(); return; } var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); - CurrentSkinInfo.Value = chosen.ToLive(); + CurrentSkinInfo.Value = chosen.ToLive(contextFactory); } } @@ -182,7 +182,7 @@ namespace osu.Game.Skinning public ILive Query(Expression> query) { using (var context = contextFactory.CreateContext()) - return context.All().FirstOrDefault(query)?.ToLive(); + return context.All().FirstOrDefault(query)?.ToLive(contextFactory); } public event Action SourceChanged; @@ -237,6 +237,7 @@ namespace osu.Game.Skinning AudioManager IStorageResourceProvider.AudioManager => audio; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.Files => userFiles; + RealmContextFactory IStorageResourceProvider.RealmContextFactory => contextFactory; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); #endregion @@ -302,7 +303,7 @@ namespace osu.Game.Skinning Guid currentUserSkin = CurrentSkinInfo.Value.ID; if (items.Any(s => s.ID == currentUserSkin)) - scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLive()); + scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()); skinModelManager.Delete(items.ToList(), silent); } diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 1681dad750..4aca079e2e 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -352,7 +352,7 @@ namespace osu.Game.Stores transaction.Commit(); } - return existing.ToLive(); + return existing.ToLive(ContextFactory); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -387,7 +387,7 @@ namespace osu.Game.Stores existing.DeletePending = false; transaction.Commit(); - return existing.ToLive(); + return existing.ToLive(ContextFactory); } LogForModel(item, @"Found existing but failed re-use check."); @@ -416,7 +416,7 @@ namespace osu.Game.Stores throw; } - return item.ToLive(); + return item.ToLive(ContextFactory); } } From 9d85beddbe5c45fa3985a6994f9dab05267eab9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 11:16:37 +0900 Subject: [PATCH 08/15] Fix null reference in some tests due to missing realm context factory --- osu.Game/Skinning/Skin.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index ee92b6b40a..d606d94b97 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,7 +43,11 @@ namespace osu.Game.Skinning protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = skin.ToLive(resources.RealmContextFactory); + SkinInfo = resources?.RealmContextFactory != null + ? skin.ToLive(resources.RealmContextFactory) + // This path should only be used in some tests. + : skin.ToLiveUnmanaged(); + this.resources = resources; configurationStream ??= getConfigurationStream(); From 0c11fe741309f8b990f4d85b69080dd9beb53f55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Dec 2021 12:45:09 +0900 Subject: [PATCH 09/15] Fix toast popups spamming samples when adjusting osu!mania scroll speed during gameplay Not the most robust of fixes, but as per the reasoning described in the issue thread, a proper fix will take considerably more effort. This intends to fix the issue first and foremost, as it sounds so bad I'd want to mute my sound before adjusting currently. Closes #15718. --- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 24 ++++++++++++++++++++ osu.Game/Overlays/OnScreenDisplay.cs | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index 51214fe460..9939ba024e 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -5,12 +5,14 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; using osu.Game.Graphics; using osuTK; using osuTK.Graphics; @@ -28,6 +30,8 @@ namespace osu.Game.Overlays.OSD private Sample sampleOff; private Sample sampleChange; + private Bindable lastPlaybackTime; + public TrackedSettingToast(SettingDescription description) : base(description.Name, description.Value, description.Shortcut) { @@ -75,10 +79,28 @@ namespace osu.Game.Overlays.OSD optionLights.Add(new OptionLight { Glowing = i == selectedOption }); } + [Resolved] + private SessionStatics statics { get; set; } + protected override void LoadComplete() { base.LoadComplete(); + playSound(); + } + + private void playSound() + { + // This debounce code roughly follows what we're using in HoverSampleDebounceComponent. + // We're sharing the existing static for hover sounds because it doesn't really matter if they block each other. + // This is a simple solution, but if this ever becomes a problem (or other performance issues arise), + // the whole toast system should be rewritten to avoid recreating this drawable each time a value changes. + lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); + + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + + if (!enoughTimePassedSinceLastPlayback) return; + if (optionCount == 1) { if (selectedOption == 0) @@ -93,6 +115,8 @@ namespace osu.Game.Overlays.OSD sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f; sampleChange.Play(); } + + lastPlaybackTime.Value = Time.Current; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index be9d3cd794..6b3696ced9 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -101,7 +101,7 @@ namespace osu.Game.Overlays DisplayTemporarily(box); }); - private void displayTrackedSettingChange(SettingDescription description) => Display(new TrackedSettingToast(description)); + private void displayTrackedSettingChange(SettingDescription description) => Scheduler.AddOnce(Display, new TrackedSettingToast(description)); private TransformSequence fadeIn; private ScheduledDelegate fadeOut; From b3e83a47a4651442ddd499d66f63a7b96c6173bf Mon Sep 17 00:00:00 2001 From: Imad Dodin Date: Wed, 15 Dec 2021 21:34:59 -0800 Subject: [PATCH 10/15] Convert to Local Time in Date Tooltip --- osu.Game/Graphics/DateTooltip.cs | 6 ++++-- osu.Game/Graphics/DrawableDate.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs index 3094f9cc2b..d5768b259a 100644 --- a/osu.Game/Graphics/DateTooltip.cs +++ b/osu.Game/Graphics/DateTooltip.cs @@ -65,8 +65,10 @@ namespace osu.Game.Graphics public void SetContent(DateTimeOffset date) { - dateText.Text = $"{date:d MMMM yyyy} "; - timeText.Text = $"{date:HH:mm:ss \"UTC\"z}"; + DateTimeOffset localDate = date.ToLocalTime(); + + dateText.Text = $"{localDate:d MMMM yyyy} "; + timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}"; } public void Move(Vector2 pos) => Position = pos; diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 567a39b4f4..4605976692 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -22,7 +22,7 @@ namespace osu.Game.Graphics if (date == value) return; - date = value.ToLocalTime(); + date = value; if (LoadState >= LoadState.Ready) updateTime(); From a9dbcd92a1fc05a8bbb800f1f0c5a4c7c2f147f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 15:11:48 +0900 Subject: [PATCH 11/15] Split out unmanaged implementation of `RealmLive` into its own class --- osu.Game/Database/RealmLive.cs | 16 ++------ osu.Game/Database/RealmLiveUnmanaged.cs | 44 ++++++++++++++++++++++ osu.Game/Database/RealmObjectExtensions.cs | 4 +- 3 files changed, 49 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Database/RealmLiveUnmanaged.cs diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 4f7bdf93e4..90b8814c24 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -24,20 +24,16 @@ namespace osu.Game.Database /// private readonly T data; - private readonly RealmContextFactory? realmFactory; + private readonly RealmContextFactory realmFactory; /// /// Construct a new instance of live realm data. /// /// The realm data. /// The realm factory the data was sourced from. May be null for an unmanaged object. - public RealmLive(T data, RealmContextFactory? realmFactory) + public RealmLive(T data, RealmContextFactory realmFactory) { this.data = data; - - if (IsManaged && realmFactory == null) - throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); - this.realmFactory = realmFactory; ID = data.ID; @@ -55,9 +51,6 @@ namespace osu.Game.Database return; } - if (realmFactory == null) - throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); - using (var realm = realmFactory.CreateContext()) perform(realm.Find(ID)); } @@ -74,9 +67,6 @@ namespace osu.Game.Database if (!IsManaged) return perform(data); - if (realmFactory == null) - throw new ArgumentException(@"Realm factory must be provided for a managed instance", nameof(realmFactory)); - using (var realm = realmFactory.CreateContext()) return perform(realm.Find(ID)); } @@ -108,7 +98,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); - return realmFactory!.Context.Find(ID); + return realmFactory.Context.Find(ID); } } diff --git a/osu.Game/Database/RealmLiveUnmanaged.cs b/osu.Game/Database/RealmLiveUnmanaged.cs new file mode 100644 index 0000000000..5a69898206 --- /dev/null +++ b/osu.Game/Database/RealmLiveUnmanaged.cs @@ -0,0 +1,44 @@ +// 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 Realms; + +#nullable enable + +namespace osu.Game.Database +{ + /// + /// Provides a method of working with unmanaged realm objects. + /// Usually used for testing purposes where the instance is never required to be managed. + /// + /// The underlying object type. + public class RealmLiveUnmanaged : ILive where T : RealmObjectBase, IHasGuidPrimaryKey + { + /// + /// Construct a new instance of live realm data. + /// + /// The realm data. + public RealmLiveUnmanaged(T data) + { + Value = data; + } + + public bool Equals(ILive? other) => ID == other?.ID; + + public Guid ID => Value.ID; + + public void PerformRead(Action perform) => perform(Value); + + public TReturn PerformRead(Func perform) => perform(Value); + + public void PerformWrite(Action perform) => throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value"); + + public bool IsManaged => false; + + /// + /// The original live data used to create this instance. + /// + public T Value { get; } + } +} diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index c546a70fae..e5177823ba 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -56,13 +56,13 @@ namespace osu.Game.Database public static List> ToLiveUnmanaged(this IEnumerable realmList) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLive(l, null)).Cast>().ToList(); + return realmList.Select(l => new RealmLiveUnmanaged(l)).Cast>().ToList(); } public static ILive ToLiveUnmanaged(this T realmObject) where T : RealmObject, IHasGuidPrimaryKey { - return new RealmLive(realmObject, null); + return new RealmLiveUnmanaged(realmObject); } public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory) From 488374b4a27c56b0f26061d55e3f7648236e3354 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Dec 2021 16:41:47 +0900 Subject: [PATCH 12/15] Don't show multiplayer channels in chat overlay --- .../Visual/Online/TestSceneChatOverlay.cs | 19 +++++++++++++++++++ osu.Game/Overlays/ChatOverlay.cs | 14 +++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 9c65b2dc51..14f32df653 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -393,6 +393,25 @@ namespace osu.Game.Tests.Visual.Online channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body"); } + [Test] + public void TestMultiplayerChannelIsNotShown() + { + Channel multiplayerChannel = null; + + AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) + { + Name = "#mp_1", + Type = ChannelType.Multiplayer, + })); + + AddAssert("channel joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel)); + AddAssert("channel not present in overlay", () => !chatOverlay.TabMap.ContainsKey(multiplayerChannel)); + AddAssert("multiplayer channel is not current", () => channelManager.CurrentChannel.Value != multiplayerChannel); + + AddStep("leave channel", () => channelManager.LeaveChannel(multiplayerChannel)); + AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel)); + } + private void pressChannelHotkey(int number) { var channelKey = Key.Number0 + number; diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index cc3ce63bf7..72473d5750 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -237,10 +237,7 @@ namespace osu.Game.Overlays Schedule(() => { // TODO: consider scheduling bindable callbacks to not perform when overlay is not present. - channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged; - - foreach (Channel channel in channelManager.JoinedChannels) - ChannelTabControl.AddChannel(channel); + channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true); channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged; availableChannelsChanged(null, null); @@ -436,12 +433,19 @@ namespace osu.Game.Overlays { case NotifyCollectionChangedAction.Add: foreach (Channel channel in args.NewItems.Cast()) - ChannelTabControl.AddChannel(channel); + { + if (channel.Type != ChannelType.Multiplayer) + ChannelTabControl.AddChannel(channel); + } + break; case NotifyCollectionChangedAction.Remove: foreach (Channel channel in args.OldItems.Cast()) { + if (!ChannelTabControl.Items.Contains(channel)) + continue; + ChannelTabControl.RemoveChannel(channel); var loaded = loadedChannels.Find(c => c.Channel == channel); From 434aa0367f4c58349bbc05258a02a99d233ef54f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 18:25:28 +0900 Subject: [PATCH 13/15] Add back `.ToLocalTime()` call to `DrawableDate` This is required because the class is used in many other places that don't locally call it. --- osu.Game/Graphics/DrawableDate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 4605976692..567a39b4f4 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -22,7 +22,7 @@ namespace osu.Game.Graphics if (date == value) return; - date = value; + date = value.ToLocalTime(); if (LoadState >= LoadState.Ready) updateTime(); From abb617a3df76c0ed685f9b84ae4abd4b3ea8be79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 19:57:24 +0900 Subject: [PATCH 14/15] Avoid blocking `Active` state propagation --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index f5fc3de381..5c6b907e42 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges.Events; @@ -44,8 +45,10 @@ namespace osu.Game.Rulesets.Osu { if (!AllowUserCursorMovement) { - // Still allow for forwarding of the "touch" part, but block the positional data. - e = new TouchStateChangeEvent(e.State, e.Input, e.Touch, false, null); + // Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse. + // Primarily relied upon by the "autopilot" osu! mod. + var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position); + e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null); } return base.HandleMouseTouchStateChange(e); From eecb1ce9f55084d19eea03378b9cf6c34cfe9706 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Dec 2021 20:20:02 +0900 Subject: [PATCH 15/15] Avoid applying mouse down effects to menu cursor when it isn't visible Closes #16114. --- osu.Game/Graphics/Cursor/MenuCursor.cs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 3fa90e2330..8e272f637f 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -72,18 +72,21 @@ namespace osu.Game.Graphics.Cursor protected override bool OnMouseDown(MouseDownEvent e) { - // only trigger animation for main mouse buttons - activeCursor.Scale = new Vector2(1); - activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint); - - activeCursor.AdditiveLayer.Alpha = 0; - activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); - - if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating) + if (State.Value == Visibility.Visible) { - // if cursor is already rotating don't reset its rotate origin - dragRotationState = DragRotationState.DragStarted; - positionMouseDown = e.MousePosition; + // only trigger animation for main mouse buttons + activeCursor.Scale = new Vector2(1); + activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint); + + activeCursor.AdditiveLayer.Alpha = 0; + activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); + + if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating) + { + // if cursor is already rotating don't reset its rotate origin + dragRotationState = DragRotationState.DragStarted; + positionMouseDown = e.MousePosition; + } } return base.OnMouseDown(e);