From 3ab04d98f618df16362e5caf25c908df07012226 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 16:45:37 +0900 Subject: [PATCH 1/4] Fix Realm-related iOS crashes by removing object references --- osu.Game/Beatmaps/BeatmapImporter.cs | 4 +++- osu.Game/Online/BeatmapDownloadTracker.cs | 4 +++- osu.Game/Online/ScoreDownloadTracker.cs | 10 +++++++--- osu.Game/Skinning/RealmBackedResourceStore.cs | 4 +++- osu.Game/Skinning/SkinManager.cs | 4 +++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 8acaebd1a8bb..63d8215d73c0 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -198,8 +198,10 @@ protected override void PreImport(BeatmapSetInfo beatmapSet, Realm realm) if (beatmapSet.OnlineID > 0) { + int onlineId = beatmapSet.OnlineID; + // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure. - foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == beatmapSet.OnlineID)) + foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == onlineId)) { existingSetWithSameOnlineID.DeletePending = true; existingSetWithSameOnlineID.OnlineID = -1; diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 3db602c35352..c1c3d17ff6e2 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,7 +40,9 @@ protected override void LoadComplete() // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _) => + int onlineId = TrackedItem.OnlineID; + + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == onlineId && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index dfdac24d1950..eb687a702300 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -46,10 +46,14 @@ protected override void LoadComplete() Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; + long onlineId = TrackedItem.OnlineID; + long legacyOnlineId = TrackedItem.LegacyOnlineID; + string hash = TrackedItem.Hash; + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => - ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) - || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID) - || (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) + ((s.OnlineID > 0 && s.OnlineID == onlineId) + || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == legacyOnlineId) + || (!string.IsNullOrEmpty(s.Hash) && s.Hash == hash)) && !s.DeletePending), (items, _) => { if (items.Any()) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index cce099a26816..b1289bd0c5ef 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -29,7 +29,9 @@ public RealmBackedResourceStore(Live source, IResourceStore underlyin invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); - realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == source.ID), skinChanged); + Guid id = source.ID; + + realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == id), skinChanged); } protected override void Dispose(bool disposing) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 4f816d88d206..cd431bd80cc0 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -131,9 +131,11 @@ public void SelectRandomSkin() { Realm.Run(r => { + Guid currentSkinId = CurrentSkinInfo.Value.ID; + // choose from only user skins, removing the current selection to ensure a new one is chosen. var randomChoices = r.All() - .Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID) + .Where(s => !s.DeletePending && s.ID != currentSkinId) .ToArray(); if (randomChoices.Length == 0) From fd4891cf31de0f5e75b937fdf168a17c16e65a3b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 20:34:50 +0900 Subject: [PATCH 2/4] Fix similar Bindable-related crashes --- .../Utils/BindableValueAccessorTest.cs | 52 +++++++++++++++++++ .../Configuration/SettingSourceAttribute.cs | 7 +-- osu.Game/Rulesets/Mods/Mod.cs | 3 +- osu.Game/Utils/BindableValueAccessor.cs | 44 ++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Tests/Utils/BindableValueAccessorTest.cs create mode 100644 osu.Game/Utils/BindableValueAccessor.cs diff --git a/osu.Game.Tests/Utils/BindableValueAccessorTest.cs b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs new file mode 100644 index 000000000000..f09623dbfc4c --- /dev/null +++ b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Utils; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class BindableValueAccessorTest + { + [Test] + public void GetValue() + { + const int value = 1337; + + BindableInt bindable = new BindableInt(value); + Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value)); + } + + [Test] + public void SetValue() + { + const int value = 1337; + + BindableInt bindable = new BindableInt(); + BindableValueAccessor.SetValue(bindable, value); + + Assert.That(bindable.Value, Is.EqualTo(value)); + } + + [Test] + public void GetInvalidBindable() + { + BindableList list = new BindableList(); + Assert.That(BindableValueAccessor.GetValue(list), Is.EqualTo(list)); + } + + [Test] + public void SetInvalidBindable() + { + const int value = 1337; + + BindableList list = new BindableList { value }; + BindableValueAccessor.SetValue(list, 2); + + Assert.That(list, Has.Exactly(1).Items); + Assert.That(list[0], Is.EqualTo(value)); + } + } +} diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1e425c88a6c3..580366a75a55 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -15,6 +14,7 @@ using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; +using osu.Game.Utils; namespace osu.Game.Configuration { @@ -228,10 +228,7 @@ public static object GetUnderlyingSettingValue(this object setting) return b.Value; case IBindable u: - // An unknown (e.g. enum) generic type. - var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); - Debug.Assert(valueMethod != null); - return valueMethod.GetValue(u)!; + return BindableValueAccessor.GetValue(u); default: // fall back for non-bindable cases. diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b9a937b1a2fb..1b21216235d8 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -266,8 +266,7 @@ public void CopyCommonSettingsFrom(Mod source) // TODO: special case for handling number types - PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable.Value))!; - property.SetValue(targetSetting, property.GetValue(sourceSetting)); + BindableValueAccessor.SetValue(targetSetting, BindableValueAccessor.GetValue(sourceSetting)); } } diff --git a/osu.Game/Utils/BindableValueAccessor.cs b/osu.Game/Utils/BindableValueAccessor.cs new file mode 100644 index 000000000000..dd097ada362e --- /dev/null +++ b/osu.Game/Utils/BindableValueAccessor.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 System.Linq; +using System.Reflection; +using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; + +namespace osu.Game.Utils +{ + internal static class BindableValueAccessor + { + private static readonly MethodInfo get_method = typeof(BindableValueAccessor).GetMethod(nameof(getValue), BindingFlags.Static | BindingFlags.NonPublic)!; + private static readonly MethodInfo set_method = typeof(BindableValueAccessor).GetMethod(nameof(setValue), BindingFlags.Static | BindingFlags.NonPublic)!; + + public static object GetValue(IBindable bindable) + { + Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(isBindableT); + if (bindableWithValueType == null) + return bindable; + + return get_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable])!; + } + + public static void SetValue(IBindable bindable, object value) + { + Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().FirstOrDefault(isBindableT); + if (bindableWithValueType == null) + return; + + set_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable, value]); + } + + private static bool isBindableT(Type type) + => type.IsGenericType + && (type.GetGenericTypeDefinition() == typeof(Bindable<>) + || type.GetGenericTypeDefinition() == typeof(IBindable<>)); + + private static object getValue(object bindable) => ((IBindable)bindable).Value!; + + private static object setValue(object bindable, object value) => ((Bindable)bindable).Value = (T)value; + } +} From 2fe229d62073a80c1a9f07155a510d9f80713584 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 22:46:53 +0900 Subject: [PATCH 3/4] Inline condition --- osu.Game/Utils/BindableValueAccessor.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Utils/BindableValueAccessor.cs b/osu.Game/Utils/BindableValueAccessor.cs index dd097ada362e..a4cd35633977 100644 --- a/osu.Game/Utils/BindableValueAccessor.cs +++ b/osu.Game/Utils/BindableValueAccessor.cs @@ -16,7 +16,7 @@ internal static class BindableValueAccessor public static object GetValue(IBindable bindable) { - Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(isBindableT); + Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IBindable<>)); if (bindableWithValueType == null) return bindable; @@ -25,18 +25,13 @@ public static object GetValue(IBindable bindable) public static void SetValue(IBindable bindable, object value) { - Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().FirstOrDefault(isBindableT); + Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().SingleOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Bindable<>)); if (bindableWithValueType == null) return; set_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable, value]); } - private static bool isBindableT(Type type) - => type.IsGenericType - && (type.GetGenericTypeDefinition() == typeof(Bindable<>) - || type.GetGenericTypeDefinition() == typeof(IBindable<>)); - private static object getValue(object bindable) => ((IBindable)bindable).Value!; private static object setValue(object bindable, object value) => ((Bindable)bindable).Value = (T)value; From 89e8baf1d35dabc6133d609e9af5e1da50cff976 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Sep 2024 16:48:42 +0900 Subject: [PATCH 4/4] Add inline comments for iOS locals --- osu.Game/Beatmaps/BeatmapImporter.cs | 1 + osu.Game/Online/BeatmapDownloadTracker.cs | 1 + osu.Game/Online/ScoreDownloadTracker.cs | 1 + osu.Game/Skinning/RealmBackedResourceStore.cs | 1 + osu.Game/Skinning/SkinManager.cs | 1 + 5 files changed, 5 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 63d8215d73c0..94144e46952f 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -198,6 +198,7 @@ protected override void PreImport(BeatmapSetInfo beatmapSet, Realm realm) if (beatmapSet.OnlineID > 0) { + // Required local for iOS. Will cause runtime crash if inlined. int onlineId = beatmapSet.OnlineID; // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure. diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index c1c3d17ff6e2..6a2163c3a298 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,6 +40,7 @@ protected override void LoadComplete() // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; + // Required local for iOS. Will cause runtime crash if inlined. int onlineId = TrackedItem.OnlineID; realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == onlineId && !s.DeletePending), (items, _) => diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index eb687a702300..5f6ba15d0564 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -46,6 +46,7 @@ protected override void LoadComplete() Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; + // Required local for iOS. Will cause runtime crash if inlined. long onlineId = TrackedItem.OnlineID; long legacyOnlineId = TrackedItem.LegacyOnlineID; string hash = TrackedItem.Hash; diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index b1289bd0c5ef..f41bd89b7a0d 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -29,6 +29,7 @@ public RealmBackedResourceStore(Live source, IResourceStore underlyin invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); + // Required local for iOS. Will cause runtime crash if inlined. Guid id = source.ID; realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == id), skinChanged); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index cd431bd80cc0..9018c2e2c30f 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -131,6 +131,7 @@ public void SelectRandomSkin() { Realm.Run(r => { + // Required local for iOS. Will cause runtime crash if inlined. Guid currentSkinId = CurrentSkinInfo.Value.ID; // choose from only user skins, removing the current selection to ensure a new one is chosen.