From ee103c9a1e4804744bed343d7a647f2d8e6a023f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 16:59:01 +0900 Subject: [PATCH 001/107] Add prerelease realm package --- osu.Game/osu.Game.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f28a55e016..eeacb10d14 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -28,6 +28,7 @@ + From dce9937e9b93898aa28fffe227ad45578f021e7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 14:19:14 +0900 Subject: [PATCH 002/107] Add automapper for detaching support --- osu.Game/osu.Game.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index eeacb10d14..1a762be9c9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,6 +18,7 @@ + From 9cfede2e7e57305d6916b058eef96bf19de254f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 14:07:36 +0900 Subject: [PATCH 003/107] Setup context, write usage, wrapper classes --- osu.Game/Database/IHasGuidPrimaryKey.cs | 16 ++ osu.Game/Database/IRealmFactory.cs | 19 ++ osu.Game/Database/RealmBackedStore.cs | 27 +++ osu.Game/Database/RealmContextFactory.cs | 262 +++++++++++++++++++++++ osu.Game/Database/RealmWriteUsage.cs | 55 +++++ osu.Game/OsuGameBase.cs | 4 + 6 files changed, 383 insertions(+) create mode 100644 osu.Game/Database/IHasGuidPrimaryKey.cs create mode 100644 osu.Game/Database/IRealmFactory.cs create mode 100644 osu.Game/Database/RealmBackedStore.cs create mode 100644 osu.Game/Database/RealmContextFactory.cs create mode 100644 osu.Game/Database/RealmWriteUsage.cs diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs new file mode 100644 index 0000000000..3dda32a5b0 --- /dev/null +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -0,0 +1,16 @@ +// 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.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace osu.Game.Database +{ + public interface IHasGuidPrimaryKey + { + [JsonIgnore] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + Guid ID { get; set; } + } +} diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs new file mode 100644 index 0000000000..d65bcaebbe --- /dev/null +++ b/osu.Game/Database/IRealmFactory.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Realms; + +namespace osu.Game.Database +{ + public interface IRealmFactory + { + public Realm Get() => Realm.GetInstance(); + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + RealmWriteUsage GetForWrite(); + } +} diff --git a/osu.Game/Database/RealmBackedStore.cs b/osu.Game/Database/RealmBackedStore.cs new file mode 100644 index 0000000000..e37831d9d5 --- /dev/null +++ b/osu.Game/Database/RealmBackedStore.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform; + +namespace osu.Game.Database +{ + public abstract class RealmBackedStore + { + protected readonly Storage Storage; + + protected readonly IRealmFactory ContextFactory; + + protected RealmBackedStore(IRealmFactory contextFactory, Storage storage = null) + { + ContextFactory = contextFactory; + Storage = storage; + } + + /// + /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. + /// + public virtual void Cleanup() + { + } + } +} diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs new file mode 100644 index 0000000000..826e098669 --- /dev/null +++ b/osu.Game/Database/RealmContextFactory.cs @@ -0,0 +1,262 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using AutoMapper; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using osu.Framework.Threading; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Input.Bindings; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Skinning; +using Realms; + +namespace osu.Game.Database +{ + public class RealmContextFactory : IRealmFactory + { + private readonly Storage storage; + private readonly Scheduler scheduler; + + private const string database_name = @"client"; + + private ThreadLocal threadContexts; + + private readonly object writeLock = new object(); + + private ThreadLocal refreshCompleted = new ThreadLocal(); + + private bool rollbackRequired; + + private int currentWriteUsages; + + private Transaction currentWriteTransaction; + + public RealmContextFactory(Storage storage, Scheduler scheduler) + { + this.storage = storage; + this.scheduler = scheduler; + recreateThreadContexts(); + } + + private static readonly GlobalStatistic reads = GlobalStatistics.Get("Database", "Get (Read)"); + private static readonly GlobalStatistic writes = GlobalStatistics.Get("Database", "Get (Write)"); + private static readonly GlobalStatistic commits = GlobalStatistics.Get("Database", "Commits"); + private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Database", "Rollbacks"); + private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Database", "Contexts"); + private Thread writingThread; + + /// + /// Get a context for the current thread for read-only usage. + /// If a is in progress, the existing write-safe context will be returned. + /// + public Realm Get() + { + reads.Value++; + return getContextForCurrentThread(); + } + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + public RealmWriteUsage GetForWrite() + { + writes.Value++; + Monitor.Enter(writeLock); + Realm context; + + try + { + context = getContextForCurrentThread(); + + if (currentWriteTransaction == null) + { + writingThread = Thread.CurrentThread; + currentWriteTransaction = context.BeginWrite(); + } + } + catch + { + // retrieval of a context could trigger a fatal error. + Monitor.Exit(writeLock); + throw; + } + + Interlocked.Increment(ref currentWriteUsages); + + return new RealmWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; + } + + // TODO: remove if not necessary. + public void Schedule(Action action) => scheduler.Add(action); + + private Realm getContextForCurrentThread() + { + var context = threadContexts.Value; + if (context?.IsClosed != false) + threadContexts.Value = context = CreateContext(); + + if (!refreshCompleted.Value) + { + context.Refresh(); + refreshCompleted.Value = true; + } + + return context; + } + + private void usageCompleted(RealmWriteUsage usage) + { + int usages = Interlocked.Decrement(ref currentWriteUsages); + + try + { + rollbackRequired |= usage.RollbackRequired; + + if (usages == 0) + { + if (rollbackRequired) + { + rollbacks.Value++; + currentWriteTransaction?.Rollback(); + } + else + { + commits.Value++; + currentWriteTransaction?.Commit(); + } + + currentWriteTransaction = null; + writingThread = null; + rollbackRequired = false; + + refreshCompleted = new ThreadLocal(); + } + } + finally + { + Monitor.Exit(writeLock); + } + } + + private void recreateThreadContexts() + { + // Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed + // for other threads to use, and we rely on the finalizer inside OsuDbContext to handle their previous contexts + threadContexts?.Value.Dispose(); + threadContexts = new ThreadLocal(CreateContext, true); + } + + protected virtual Realm CreateContext() + { + contexts.Value++; + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))); + } + + public void ResetDatabase() + { + lock (writeLock) + { + recreateThreadContexts(); + storage.DeleteDatabase(database_name); + } + } + } + + [SuppressMessage("ReSharper", "CA2225")] + public class RealmWrapper : IEquatable> + where T : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; } + + private readonly ThreadLocal threadValues; + + public readonly IRealmFactory ContextFactory; + + public RealmWrapper(T original, IRealmFactory contextFactory) + { + ContextFactory = contextFactory; + ID = original.ID; + + var originalContext = original.Realm; + + threadValues = new ThreadLocal(() => + { + var context = ContextFactory?.Get(); + + if (context == null || originalContext?.IsSameInstance(context) != false) + return original; + + return context.Find(ID); + }); + } + + public T Get() => threadValues.Value; + + public RealmWrapper WrapChild(Func lookup) + where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); + + // ReSharper disable once CA2225 + public static implicit operator T(RealmWrapper wrapper) + => wrapper?.Get().Detach(); + + // ReSharper disable once CA2225 + public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); + + public bool Equals(RealmWrapper other) => other != null && other.ID == ID; + + public override string ToString() => Get().ToString(); + } + + public static class RealmExtensions + { + private static readonly IMapper mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + + c.CreateMap() + .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) + .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) + .MaxDepth(2); + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + }).CreateMapper(); + + public static T Detach(this T obj) where T : RealmObject + { + if (!obj.IsManaged) + return obj; + + var detached = mapper.Map(obj); + + //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); + + return detached; + } + + public static RealmWrapper Wrap(this T obj, IRealmFactory contextFactory) + where T : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(obj, contextFactory); + + public static RealmWrapper WrapAsUnmanaged(this T obj) + where T : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(obj, null); + } +} diff --git a/osu.Game/Database/RealmWriteUsage.cs b/osu.Game/Database/RealmWriteUsage.cs new file mode 100644 index 0000000000..35e30e8123 --- /dev/null +++ b/osu.Game/Database/RealmWriteUsage.cs @@ -0,0 +1,55 @@ +// 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; + +namespace osu.Game.Database +{ + public class RealmWriteUsage : IDisposable + { + public readonly Realm Context; + private readonly Action usageCompleted; + + public bool RollbackRequired { get; private set; } + + public RealmWriteUsage(Realm context, Action onCompleted) + { + Context = context; + usageCompleted = onCompleted; + } + + /// + /// Whether this write usage will commit a transaction on completion. + /// If false, there is a parent usage responsible for transaction commit. + /// + public bool IsTransactionLeader; + + private bool isDisposed; + + protected void Dispose(bool disposing) + { + if (isDisposed) return; + + isDisposed = true; + + usageCompleted?.Invoke(this); + } + + public void Rollback(Exception error = null) + { + RollbackRequired = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~RealmWriteUsage() + { + Dispose(false); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0b5abc4e31..8ec6976c63 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -146,6 +146,8 @@ namespace osu.Game private DatabaseContextFactory contextFactory; + private RealmContextFactory realmFactory; + protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); [BackgroundDependencyLoader] @@ -167,6 +169,8 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, Scheduler)); + dependencies.CacheAs(Storage); var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); From 5372d95d167953e812d25941d5fd44e46e47e36c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 14:29:22 +0900 Subject: [PATCH 004/107] Initialise FodyWeavers --- osu.Game/FodyWeavers.xml | 3 +++ osu.Game/FodyWeavers.xsd | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 osu.Game/FodyWeavers.xml create mode 100644 osu.Game/FodyWeavers.xsd diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml new file mode 100644 index 0000000000..cc07b89533 --- /dev/null +++ b/osu.Game/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/osu.Game/FodyWeavers.xsd b/osu.Game/FodyWeavers.xsd new file mode 100644 index 0000000000..f526bddb09 --- /dev/null +++ b/osu.Game/FodyWeavers.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file From d5ac97ece849f46711e97e5e0e290119a4370ef2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 14:35:15 +0900 Subject: [PATCH 005/107] Add realm store / key binding implementations --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 33 ++++++++ osu.Game/Input/RealmKeyBindingStore.cs | 92 ++++++++++++++++++++++ osu.Game/OsuGameBase.cs | 1 + 3 files changed, 126 insertions(+) create mode 100644 osu.Game/Input/Bindings/RealmKeyBinding.cs create mode 100644 osu.Game/Input/RealmKeyBindingStore.cs diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs new file mode 100644 index 0000000000..a8cd1c3fb6 --- /dev/null +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -0,0 +1,33 @@ +// 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.Framework.Input.Bindings; +using osu.Game.Database; +using Realms; + +namespace osu.Game.Input.Bindings +{ + [MapTo("KeyBinding")] + public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; set; } + + public int? RulesetID { get; set; } + + public int? Variant { get; set; } + + [Ignored] + public KeyBinding KeyBinding + { + get + { + var split = KeyBindingString.Split(':'); + return new KeyBinding(split[0], int.Parse(split[1])); + } + set => KeyBindingString = $"{value.KeyCombination}:{(int)value.Action}"; + } + + public string KeyBindingString { get; set; } + } +} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs new file mode 100644 index 0000000000..471a25dd0d --- /dev/null +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; + +namespace osu.Game.Input +{ + public class RealmKeyBindingStore : RealmBackedStore + { + public event Action KeyBindingChanged; + + public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) + : base(contextFactory, storage) + { + using (ContextFactory.GetForWrite()) + { + foreach (RulesetInfo info in rulesets.AvailableRulesets) + { + var ruleset = info.CreateInstance(); + foreach (var variant in ruleset.AvailableVariants) + insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + } + } + } + + public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) + { + using (var usage = ContextFactory.GetForWrite()) + { + // compare counts in database vs defaults + foreach (var group in defaults.GroupBy(k => k.Action)) + { + int count = Query(rulesetId, variant).Count(k => k.KeyBinding.Action == group.Key); + int aimCount = group.Count(); + + if (aimCount <= count) + continue; + + foreach (var insertable in group.Skip(count).Take(aimCount - count)) + { + // insert any defaults which are missing. + usage.Context.Add(new RealmKeyBinding + { + KeyBinding = new KeyBinding + { + KeyCombination = insertable.KeyCombination, + Action = insertable.Action, + }, + RulesetID = rulesetId, + Variant = variant + }); + } + } + } + } + + /// + /// Retrieve s for a specified ruleset/variant content. + /// + /// The ruleset's internal ID. + /// An optional variant. + /// + public List Query(int? rulesetId = null, int? variant = null) => + ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + + public void Update(KeyBinding keyBinding) + { + using (ContextFactory.GetForWrite()) + { + //todo: fix + // var dbKeyBinding = (RealmKeyBinding)keyBinding; + // Refresh(ref dbKeyBinding); + // + // if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) + // return; + // + // dbKeyBinding.KeyCombination = keyBinding.KeyCombination; + } + + KeyBindingChanged?.Invoke(); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8ec6976c63..95b1d3100c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -266,6 +266,7 @@ namespace osu.Game AddInternal(scorePerformanceManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + dependencies.Cache(new RealmKeyBindingStore(realmFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); From 5d7ab4a7f12b7a07b4b2b1779908512307433f1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:01:41 +0900 Subject: [PATCH 006/107] Rename global statistics to be specific to realm --- osu.Game/Database/RealmContextFactory.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 826e098669..c37068947e 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -45,11 +45,11 @@ namespace osu.Game.Database recreateThreadContexts(); } - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Database", "Get (Read)"); - private static readonly GlobalStatistic writes = GlobalStatistics.Get("Database", "Get (Write)"); - private static readonly GlobalStatistic commits = GlobalStatistics.Get("Database", "Commits"); - private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Database", "Rollbacks"); - private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Database", "Contexts"); + private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); + private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); + private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); + private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); + private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Realm", "Contexts"); private Thread writingThread; /// From ae76eca5648525abf580b4dfa6ebc78eb69fd423 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:41:29 +0900 Subject: [PATCH 007/107] Add basic realm migration support --- osu.Game/Database/RealmContextFactory.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c37068947e..b918eb0e78 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -43,6 +43,15 @@ namespace osu.Game.Database this.storage = storage; this.scheduler = scheduler; recreateThreadContexts(); + + using (CreateContext()) + { + // ensure our schema is up-to-date and migrated. + } + } + + private void onMigration(Migration migration, ulong oldschemaversion) + { } private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); @@ -158,7 +167,11 @@ namespace osu.Game.Database protected virtual Realm CreateContext() { contexts.Value++; - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))); + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + { + SchemaVersion = 2, + MigrationCallback = onMigration + }); } public void ResetDatabase() From 845d5cdea23bf0fc179b3a1226c109d2cfbd6b94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:41:58 +0900 Subject: [PATCH 008/107] Switch guid to store as string until fody issues are resolved See https://github.com/realm/realm-dotnet/issues/740#issuecomment-755898968 --- osu.Game/Database/IHasGuidPrimaryKey.cs | 11 ++++++++++- osu.Game/Database/RealmContextFactory.cs | 2 +- osu.Game/Input/Bindings/RealmKeyBinding.cs | 3 +-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index 3dda32a5b0..33618e990d 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -4,13 +4,22 @@ using System; using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; +using Realms; namespace osu.Game.Database { public interface IHasGuidPrimaryKey { + [JsonIgnore] + [Ignored] + public Guid Guid + { + get => new Guid(ID); + set => ID = value.ToString(); + } + [JsonIgnore] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - Guid ID { get; set; } + string ID { get; set; } } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index b918eb0e78..feb03c1609 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -197,7 +197,7 @@ namespace osu.Game.Database public RealmWrapper(T original, IRealmFactory contextFactory) { ContextFactory = contextFactory; - ID = original.ID; + ID = original.Guid; var originalContext = original.Realm; diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index a8cd1c3fb6..332e4e2b21 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Input.Bindings; using osu.Game.Database; using Realms; @@ -11,7 +10,7 @@ namespace osu.Game.Input.Bindings [MapTo("KeyBinding")] public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey { - public Guid ID { get; set; } + public string ID { get; set; } public int? RulesetID { get; set; } From ee6a26bd6eff8988b30d061eb4b2bcbbaddbd755 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:42:21 +0900 Subject: [PATCH 009/107] Initialise new key bindings with a primary key --- osu.Game/Input/RealmKeyBindingStore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 471a25dd0d..752e254a43 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -50,6 +50,7 @@ namespace osu.Game.Input // insert any defaults which are missing. usage.Context.Add(new RealmKeyBinding { + ID = Guid.NewGuid().ToString(), KeyBinding = new KeyBinding { KeyCombination = insertable.KeyCombination, From 391259c713cb9b8303839ea4872c42f4345ea197 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:51:16 +0900 Subject: [PATCH 010/107] Add missing implementation details to realm keybinding store --- osu.Game/Input/RealmKeyBindingStore.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 752e254a43..f81d701e62 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -30,6 +30,23 @@ namespace osu.Game.Input } } + /// + /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. + /// + /// The action to lookup. + /// A set of display strings for all the user's key configuration for the action. + public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) + { + foreach (var action in Query().Where(b => (GlobalAction)b.KeyBinding.Action == globalAction)) + { + string str = action.KeyBinding.KeyCombination.ReadableString(); + + // even if found, the readable string may be empty for an unbound action. + if (str.Length > 0) + yield return str; + } + } + public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) From 382a40b24375c328ec356cf802cc37b67a30e2a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:51:29 +0900 Subject: [PATCH 011/107] Tidy up some missed inspections in RealmContextFactory --- osu.Game/Database/RealmContextFactory.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index feb03c1609..8e1a0bb8f7 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -59,7 +59,6 @@ namespace osu.Game.Database private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Realm", "Contexts"); - private Thread writingThread; /// /// Get a context for the current thread for read-only usage. @@ -86,11 +85,7 @@ namespace osu.Game.Database { context = getContextForCurrentThread(); - if (currentWriteTransaction == null) - { - writingThread = Thread.CurrentThread; - currentWriteTransaction = context.BeginWrite(); - } + currentWriteTransaction ??= context.BeginWrite(); } catch { @@ -144,7 +139,6 @@ namespace osu.Game.Database } currentWriteTransaction = null; - writingThread = null; rollbackRequired = false; refreshCompleted = new ThreadLocal(); From a9a3a959914cb288dec68dedb74f2b1236c9b23d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 15:51:38 +0900 Subject: [PATCH 012/107] Replace KeybindingStore with realm version --- osu.Game/OsuGameBase.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 95b1d3100c..22e72d9f36 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -73,7 +73,7 @@ namespace osu.Game protected FileStore FileStore; - protected KeyBindingStore KeyBindingStore; + protected RealmKeyBindingStore KeyBindingStore; protected SettingsStore SettingsStore; @@ -265,8 +265,10 @@ namespace osu.Game dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); - dependencies.Cache(new RealmKeyBindingStore(realmFactory, RulesetStore)); + // todo: migrate to realm + // dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + + dependencies.Cache(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); From 43f417b53ab036b267dc4f59eab046085f3f1493 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 16:38:49 +0900 Subject: [PATCH 013/107] Add and consume IKeyBindingStore interface --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 12 ++---- osu.Game/Input/IKeyBindingStore.cs | 40 +++++++++++++++++++ osu.Game/Input/KeyBindingStore.cs | 14 +++++-- osu.Game/Input/RealmKeyBindingStore.cs | 14 +++++-- osu.Game/OsuGameBase.cs | 4 +- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 2 +- .../KeyBinding/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 16 ++++++-- 9 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 osu.Game/Input/IKeyBindingStore.cs diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index b347c39c1e..eddaf36f92 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual typeof(FileStore), typeof(ScoreManager), typeof(BeatmapManager), - typeof(KeyBindingStore), + typeof(IKeyBindingStore), typeof(SettingsStore), typeof(RulesetConfigCache), typeof(OsuColour), diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 94edc33099..edaf18a760 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Rulesets; -using System.Linq; namespace osu.Game.Input.Bindings { @@ -21,7 +20,8 @@ namespace osu.Game.Input.Bindings private readonly int? variant; - private KeyBindingStore store; + [Resolved] + private IKeyBindingStore store { get; set; } public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); @@ -42,12 +42,6 @@ namespace osu.Game.Input.Bindings throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided."); } - [BackgroundDependencyLoader] - private void load(KeyBindingStore keyBindings) - { - store = keyBindings; - } - protected override void LoadComplete() { base.LoadComplete(); @@ -69,7 +63,7 @@ namespace osu.Game.Input.Bindings // fallback to defaults instead. KeyBindings = DefaultKeyBindings; else - KeyBindings = store.Query(ruleset?.ID, variant).ToList(); + KeyBindings = store.Query(ruleset?.ID, variant); } } } diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs new file mode 100644 index 0000000000..3574c7237f --- /dev/null +++ b/osu.Game/Input/IKeyBindingStore.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; + +namespace osu.Game.Input +{ + public interface IKeyBindingStore + { + event Action KeyBindingChanged; + + void Register(KeyBindingContainer manager); + + /// + /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. + /// + /// The action to lookup. + /// A set of display strings for all the user's key configuration for the action. + IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction); + + /// + /// Retrieve s for a specified ruleset/variant content. + /// + /// The ruleset's internal ID. + /// An optional variant. + /// + List Query(int? rulesetId = null, int? variant = null); + + /// + /// Retrieve s for the specified action. + /// + /// The action to lookup. + List Query(GlobalAction action); + + public void Update(KeyBinding buttonKeyBinding) => throw new NotImplementedException(); + } +} diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index bc73d74d74..bbf26c4d8f 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets; namespace osu.Game.Input { - public class KeyBindingStore : DatabaseBackedStore + public class KeyBindingStore : DatabaseBackedStore, IKeyBindingStore { public event Action KeyBindingChanged; @@ -39,7 +39,7 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction)) + foreach (var action in query().Where(b => (GlobalAction)b.Action == globalAction)) { string str = action.KeyCombination.ReadableString(); @@ -49,6 +49,12 @@ namespace osu.Game.Input } } + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).OfType().ToList(); + + public List Query(GlobalAction action) + => query(null, null).Where(dkb => (GlobalAction)dkb.Action == action).OfType().ToList(); + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) @@ -56,7 +62,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); + int count = query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -86,7 +92,7 @@ namespace osu.Game.Input /// The ruleset's internal ID. /// An optional variant. /// - public List Query(int? rulesetId = null, int? variant = null) => + private List query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); public void Update(KeyBinding keyBinding) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index f81d701e62..07a340b25c 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets; namespace osu.Game.Input { - public class RealmKeyBindingStore : RealmBackedStore + public class RealmKeyBindingStore : RealmBackedStore, IKeyBindingStore { public event Action KeyBindingChanged; @@ -37,7 +37,7 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in Query().Where(b => (GlobalAction)b.KeyBinding.Action == globalAction)) + foreach (var action in query().Where(b => (GlobalAction)b.KeyBinding.Action == globalAction)) { string str = action.KeyBinding.KeyCombination.ReadableString(); @@ -56,7 +56,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = Query(rulesetId, variant).Count(k => k.KeyBinding.Action == group.Key); + int count = query(rulesetId, variant).Count(k => (int)k.KeyBinding.Action == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -87,9 +87,15 @@ namespace osu.Game.Input /// The ruleset's internal ID. /// An optional variant. /// - public List Query(int? rulesetId = null, int? variant = null) => + private List query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).Select(k => k.KeyBinding).ToList(); + + public List Query(GlobalAction action) + => query(null, null).Where(rkb => rkb.KeyBindingString.StartsWith($"{(int)action}:", StringComparison.Ordinal)).Select(k => k.KeyBinding).ToList(); + public void Update(KeyBinding keyBinding) { using (ContextFactory.GetForWrite()) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 22e72d9f36..95e1bc69b3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -73,7 +73,7 @@ namespace osu.Game protected FileStore FileStore; - protected RealmKeyBindingStore KeyBindingStore; + protected IKeyBindingStore KeyBindingStore; protected SettingsStore SettingsStore; @@ -268,7 +268,7 @@ namespace osu.Game // todo: migrate to realm // dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); - dependencies.Cache(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); + dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index b808d49fa2..d9f63328d0 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.KeyBinding } [Resolved] - private KeyBindingStore store { get; set; } + private IKeyBindingStore store { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index d784b7aec9..b5d6bc98c3 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.KeyBinding } [BackgroundDependencyLoader] - private void load(KeyBindingStore store) + private void load(IKeyBindingStore store) { var bindings = store.Query(Ruleset?.ID, variant); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 49b9c62d85..747f5e9bd0 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; @@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private KeyBindingStore keyBindings { get; set; } + private IKeyBindingStore keyBindings { get; set; } protected ToolbarButton() : base(HoverSampleSet.Loud) @@ -171,9 +172,16 @@ namespace osu.Game.Overlays.Toolbar if (tooltipKeyBinding.IsValid) return; - var binding = keyBindings.Query().Find(b => (GlobalAction)b.Action == Hotkey); - var keyBindingString = binding?.KeyCombination.ReadableString(); - keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty; + keyBindingTooltip.Text = string.Empty; + + if (Hotkey != null) + { + KeyCombination? binding = keyBindings.Query(Hotkey.Value).FirstOrDefault()?.KeyCombination; + var keyBindingString = binding?.ReadableString(); + + if (!string.IsNullOrEmpty(keyBindingString)) + keyBindingTooltip.Text = $" ({keyBindingString})"; + } tooltipKeyBinding.Validate(); } From a77519c6bde35c3b24e0e1888ec7059e1f149555 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 16:53:36 +0900 Subject: [PATCH 014/107] Store KeyBinding action to its own field in realm Also improve the Query method for action types by using generic field --- osu.Game/Database/RealmContextFactory.cs | 2 +- osu.Game/Input/Bindings/RealmKeyBinding.cs | 14 ++++++++------ osu.Game/Input/IKeyBindingStore.cs | 2 +- osu.Game/Input/KeyBindingStore.cs | 7 +++++-- osu.Game/Input/RealmKeyBindingStore.cs | 9 +++++++-- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 8e1a0bb8f7..2f6ccb8911 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -163,7 +163,7 @@ namespace osu.Game.Database contexts.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { - SchemaVersion = 2, + SchemaVersion = 3, MigrationCallback = onMigration }); } diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 332e4e2b21..eb04766d04 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -16,17 +16,19 @@ namespace osu.Game.Input.Bindings public int? Variant { get; set; } + public int Action { get; set; } + + public string KeyCombination { get; set; } + [Ignored] public KeyBinding KeyBinding { - get + get => new KeyBinding(KeyCombination, Action); + set { - var split = KeyBindingString.Split(':'); - return new KeyBinding(split[0], int.Parse(split[1])); + KeyCombination = value.KeyCombination.ToString(); + Action = (int)value.Action; } - set => KeyBindingString = $"{value.KeyCombination}:{(int)value.Action}"; } - - public string KeyBindingString { get; set; } } } diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs index 3574c7237f..c5e68dc6ca 100644 --- a/osu.Game/Input/IKeyBindingStore.cs +++ b/osu.Game/Input/IKeyBindingStore.cs @@ -33,7 +33,7 @@ namespace osu.Game.Input /// Retrieve s for the specified action. /// /// The action to lookup. - List Query(GlobalAction action); + List Query(T action) where T : Enum; public void Update(KeyBinding buttonKeyBinding) => throw new NotImplementedException(); } diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index bbf26c4d8f..53eb0024f8 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -52,8 +52,11 @@ namespace osu.Game.Input public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).OfType().ToList(); - public List Query(GlobalAction action) - => query(null, null).Where(dkb => (GlobalAction)dkb.Action == action).OfType().ToList(); + public List Query(T action) where T : Enum + { + int lookup = (int)(object)action; + return query(null, null).Where(rkb => (int)rkb.Action == lookup).OfType().ToList(); + } private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 07a340b25c..37d0ce18ed 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -93,8 +93,13 @@ namespace osu.Game.Input public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).Select(k => k.KeyBinding).ToList(); - public List Query(GlobalAction action) - => query(null, null).Where(rkb => rkb.KeyBindingString.StartsWith($"{(int)action}:", StringComparison.Ordinal)).Select(k => k.KeyBinding).ToList(); + public List Query(T action) + where T : Enum + { + int lookup = (int)(object)action; + + return query(null, null).Where(rkb => rkb.Action == lookup).Select(k => k.KeyBinding).ToList(); + } public void Update(KeyBinding keyBinding) { From 8765aaf9e669a88eacd31089671bfac20fd31e66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 15:49:01 +0900 Subject: [PATCH 015/107] Use IKeyBinding for all key binding usages (and add update flow via primary key) --- .../Gameplay/TestSceneReplayRecorder.cs | 2 +- .../Gameplay/TestSceneReplayRecording.cs | 2 +- .../Gameplay/TestSceneSpectatorPlayback.cs | 2 +- .../Input/Bindings/DatabasedKeyBinding.cs | 24 ++++++++----- .../Bindings/DatabasedKeyBindingContainer.cs | 2 +- .../Input/Bindings/GlobalActionContainer.cs | 2 +- osu.Game/Input/Bindings/RealmKeyBinding.cs | 15 +++++++- osu.Game/Input/IKeyBindingStore.cs | 9 ++--- osu.Game/Input/KeyBindingStore.cs | 19 +++++++---- osu.Game/Input/RealmKeyBindingStore.cs | 34 ++++++++----------- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 13 +++---- 11 files changed, 72 insertions(+), 52 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index b2ad7ca5b4..802dbf2021 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 40c4214749..6e338b7202 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index e148fa381c..a5fd5afcde 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; diff --git a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs index 8c0072c3da..ad3493d0fc 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs @@ -8,7 +8,7 @@ using osu.Game.Database; namespace osu.Game.Input.Bindings { [Table("KeyBinding")] - public class DatabasedKeyBinding : KeyBinding, IHasPrimaryKey + public class DatabasedKeyBinding : IKeyBinding, IHasPrimaryKey { public int ID { get; set; } @@ -17,17 +17,23 @@ namespace osu.Game.Input.Bindings public int? Variant { get; set; } [Column("Keys")] - public string KeysString - { - get => KeyCombination.ToString(); - private set => KeyCombination = value; - } + public string KeysString { get; set; } [Column("Action")] - public int IntAction + public int IntAction { get; set; } + + [NotMapped] + public KeyCombination KeyCombination { - get => (int)Action; - set => Action = value; + get => KeysString; + set => KeysString = value.ToString(); + } + + [NotMapped] + public object Action + { + get => IntAction; + set => IntAction = (int)value; } } } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index edaf18a760..ab4854bfd2 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Input.Bindings [Resolved] private IKeyBindingStore store { get; set; } - public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); + public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); /// /// Create a new instance. diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index b8c2fa201f..8ccdb9249e 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Input.Bindings handler = game; } - public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings); + public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings); public IEnumerable GlobalKeyBindings => new[] { diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index eb04766d04..088a314fec 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -8,14 +8,27 @@ using Realms; namespace osu.Game.Input.Bindings { [MapTo("KeyBinding")] - public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey + public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { + [PrimaryKey] public string ID { get; set; } public int? RulesetID { get; set; } public int? Variant { get; set; } + KeyCombination IKeyBinding.KeyCombination + { + get => KeyCombination; + set => KeyCombination = value.ToString(); + } + + object IKeyBinding.Action + { + get => Action; + set => Action = (int)value; + } + public int Action { get; set; } public string KeyCombination { get; set; } diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs index c5e68dc6ca..50994cb542 100644 --- a/osu.Game/Input/IKeyBindingStore.cs +++ b/osu.Game/Input/IKeyBindingStore.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Input.Bindings; +using osu.Game.Database; using osu.Game.Input.Bindings; namespace osu.Game.Input @@ -27,14 +28,14 @@ namespace osu.Game.Input /// The ruleset's internal ID. /// An optional variant. /// - List Query(int? rulesetId = null, int? variant = null); + List Query(int? rulesetId = null, int? variant = null); /// - /// Retrieve s for the specified action. + /// Retrieve s for the specified action. /// /// The action to lookup. - List Query(T action) where T : Enum; + List Query(T action) where T : Enum; - public void Update(KeyBinding buttonKeyBinding) => throw new NotImplementedException(); + void Update(IHasGuidPrimaryKey keyBinding, Action modification); } } diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 53eb0024f8..ad6bcb4c7c 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -49,16 +49,21 @@ namespace osu.Game.Input } } - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).OfType().ToList(); + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).OfType().ToList(); - public List Query(T action) where T : Enum + public List Query(T action) where T : Enum { int lookup = (int)(object)action; - return query(null, null).Where(rkb => (int)rkb.Action == lookup).OfType().ToList(); + return query(null, null).Where(rkb => (int)rkb.Action == lookup).OfType().ToList(); } - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) + public void Update(IHasGuidPrimaryKey keyBinding, Action modification) + { + throw new NotImplementedException(); + } + + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) { @@ -98,11 +103,11 @@ namespace osu.Game.Input private List query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); - public void Update(KeyBinding keyBinding) + public void Update(DatabasedKeyBinding keyBinding) { using (ContextFactory.GetForWrite()) { - var dbKeyBinding = (DatabasedKeyBinding)keyBinding; + var dbKeyBinding = keyBinding; Refresh(ref dbKeyBinding); if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 37d0ce18ed..2455578ddb 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -37,9 +37,9 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in query().Where(b => (GlobalAction)b.KeyBinding.Action == globalAction)) + foreach (var action in query().Where(b => (GlobalAction)b.Action == globalAction)) { - string str = action.KeyBinding.KeyCombination.ReadableString(); + string str = ((IKeyBinding)action).KeyCombination.ReadableString(); // even if found, the readable string may be empty for an unbound action. if (str.Length > 0) @@ -49,14 +49,14 @@ namespace osu.Game.Input public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) { // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = query(rulesetId, variant).Count(k => (int)k.KeyBinding.Action == (int)group.Key); + int count = query(rulesetId, variant).Count(k => k.Action == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -87,32 +87,26 @@ namespace osu.Game.Input /// The ruleset's internal ID. /// An optional variant. /// - private List query(int? rulesetId = null, int? variant = null) => - ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + private IQueryable query(int? rulesetId = null, int? variant = null) => + ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).Select(k => k.KeyBinding).ToList(); + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); - public List Query(T action) + public List Query(T action) where T : Enum { int lookup = (int)(object)action; - return query(null, null).Where(rkb => rkb.Action == lookup).Select(k => k.KeyBinding).ToList(); + return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); } - public void Update(KeyBinding keyBinding) + public void Update(IHasGuidPrimaryKey keyBinding, Action modification) { - using (ContextFactory.GetForWrite()) + using (var realm = ContextFactory.GetForWrite()) { - //todo: fix - // var dbKeyBinding = (RealmKeyBinding)keyBinding; - // Refresh(ref dbKeyBinding); - // - // if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) - // return; - // - // dbKeyBinding.KeyCombination = keyBinding.KeyCombination; + var realmKeyBinding = realm.Context.Find(keyBinding.ID); + modification(realmKeyBinding); } KeyBindingChanged?.Invoke(); diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index d9f63328d0..87d51e5268 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -25,7 +26,7 @@ namespace osu.Game.Overlays.KeyBinding public class KeyBindingRow : Container, IFilterable { private readonly object action; - private readonly IEnumerable bindings; + private readonly IEnumerable bindings; private const float transition_time = 150; @@ -53,7 +54,7 @@ namespace osu.Game.Overlays.KeyBinding public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); - public KeyBindingRow(object action, IEnumerable bindings) + public KeyBindingRow(object action, IEnumerable bindings) { this.action = action; this.bindings = bindings; @@ -126,7 +127,7 @@ namespace osu.Game.Overlays.KeyBinding { var button = buttons[i++]; button.UpdateKeyCombination(d); - store.Update(button.KeyBinding); + store.Update((IHasGuidPrimaryKey)button.KeyBinding, k => k.KeyCombination = button.KeyBinding.KeyCombination); } } @@ -285,7 +286,7 @@ namespace osu.Game.Overlays.KeyBinding { if (bindTarget != null) { - store.Update(bindTarget.KeyBinding); + store.Update((IHasGuidPrimaryKey)bindTarget.KeyBinding, k => k.KeyCombination = bindTarget.KeyBinding.KeyCombination); bindTarget.IsBinding = false; Schedule(() => @@ -359,7 +360,7 @@ namespace osu.Game.Overlays.KeyBinding public class KeyButton : Container { - public readonly Framework.Input.Bindings.KeyBinding KeyBinding; + public readonly IKeyBinding KeyBinding; private readonly Box box; public readonly OsuSpriteText Text; @@ -381,7 +382,7 @@ namespace osu.Game.Overlays.KeyBinding } } - public KeyButton(Framework.Input.Bindings.KeyBinding keyBinding) + public KeyButton(IKeyBinding keyBinding) { KeyBinding = keyBinding; From 86daf65630592944e9f505c4db74745d9b8fa651 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 15:49:11 +0900 Subject: [PATCH 016/107] Fix primary key not being populated for KeyBinding --- osu.Game/Database/IHasGuidPrimaryKey.cs | 1 + osu.Game/Database/RealmContextFactory.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index 33618e990d..3b0888c654 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -20,6 +20,7 @@ namespace osu.Game.Database [JsonIgnore] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [PrimaryKey] string ID { get; set; } } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 2f6ccb8911..62e2dbba9a 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -163,7 +163,7 @@ namespace osu.Game.Database contexts.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { - SchemaVersion = 3, + SchemaVersion = 5, MigrationCallback = onMigration }); } @@ -211,6 +211,12 @@ namespace osu.Game.Database public RealmWrapper WrapChild(Func lookup) where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); + public void PerformUpdate(Action perform) + { + using (ContextFactory.GetForWrite()) + perform(this); + } + // ReSharper disable once CA2225 public static implicit operator T(RealmWrapper wrapper) => wrapper?.Get().Detach(); @@ -241,6 +247,7 @@ namespace osu.Game.Database .MaxDepth(2); c.CreateMap(); + c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); From a1cb6d8c546327921b740aeb1ecc28b610177613 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 17:44:12 +0900 Subject: [PATCH 017/107] Remove unnecesssary local conversion method --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 11 ----------- osu.Game/Input/RealmKeyBindingStore.cs | 7 ++----- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 088a314fec..1e690ddbab 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -32,16 +32,5 @@ namespace osu.Game.Input.Bindings public int Action { get; set; } public string KeyCombination { get; set; } - - [Ignored] - public KeyBinding KeyBinding - { - get => new KeyBinding(KeyCombination, Action); - set - { - KeyCombination = value.KeyCombination.ToString(); - Action = (int)value.Action; - } - } } } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 2455578ddb..33172921cb 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -68,11 +68,8 @@ namespace osu.Game.Input usage.Context.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), - KeyBinding = new KeyBinding - { - KeyCombination = insertable.KeyCombination, - Action = insertable.Action, - }, + KeyCombination = insertable.KeyCombination.ToString(), + Action = (int)insertable.Action, RulesetID = rulesetId, Variant = variant }); From 1abed11fb7b80977f6af07d2525c97d23ea917c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 17:44:19 +0900 Subject: [PATCH 018/107] Add basic migration logic of key bindings to realm --- osu.Game/OsuGameBase.cs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 95e1bc69b3..3fa55ab594 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,6 +24,7 @@ using osu.Game.Online.API; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; @@ -265,10 +266,10 @@ namespace osu.Game dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); - // todo: migrate to realm - // dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + migrateDataToRealm(); dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); + dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); @@ -322,6 +323,29 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); } + private void migrateDataToRealm() + { + using (var db = contextFactory.GetForWrite()) + using (var realm = realmFactory.GetForWrite()) + { + var existingBindings = db.Context.DatabasedKeyBinding; + + foreach (var dkb in existingBindings) + { + realm.Context.Add(new RealmKeyBinding + { + ID = Guid.NewGuid().ToString(), + KeyCombination = dkb.KeyCombination.ToString(), + Action = (int)dkb.Action, + RulesetID = dkb.RulesetID, + Variant = dkb.Variant + }); + } + + db.Context.RemoveRange(existingBindings); + } + } + private void onRulesetChanged(ValueChangedEvent r) { var dict = new Dictionary>(); From 56d34432f92b486e24623172b1cc18feeccdc421 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 15:56:41 +0900 Subject: [PATCH 019/107] Move public members up --- osu.Game/Input/RealmKeyBindingStore.cs | 44 +++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 33172921cb..9910882cef 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -49,6 +49,28 @@ namespace osu.Game.Input public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + public List Query(int? rulesetId = null, int? variant = null) + => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); + + public List Query(T action) + where T : Enum + { + int lookup = (int)(object)action; + + return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); + } + + public void Update(IHasGuidPrimaryKey keyBinding, Action modification) + { + using (var realm = ContextFactory.GetForWrite()) + { + var realmKeyBinding = realm.Context.Find(keyBinding.ID); + modification(realmKeyBinding); + } + + KeyBindingChanged?.Invoke(); + } + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) @@ -86,27 +108,5 @@ namespace osu.Game.Input /// private IQueryable query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); - - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); - - public List Query(T action) - where T : Enum - { - int lookup = (int)(object)action; - - return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); - } - - public void Update(IHasGuidPrimaryKey keyBinding, Action modification) - { - using (var realm = ContextFactory.GetForWrite()) - { - var realmKeyBinding = realm.Context.Find(keyBinding.ID); - modification(realmKeyBinding); - } - - KeyBindingChanged?.Invoke(); - } } } From f9717e8b693346c1867e0f0bbc9c69bca1b8f9f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:00:11 +0900 Subject: [PATCH 020/107] Don't migrate existing key bindings across if realm is already populated --- osu.Game/OsuGameBase.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3fa55ab594..4b64bf2e24 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,7 +24,6 @@ using osu.Game.Online.API; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; -using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; @@ -330,16 +329,20 @@ namespace osu.Game { var existingBindings = db.Context.DatabasedKeyBinding; - foreach (var dkb in existingBindings) + // only migrate data if the realm database is empty. + if (!realm.Context.All().Any()) { - realm.Context.Add(new RealmKeyBinding + foreach (var dkb in existingBindings) { - ID = Guid.NewGuid().ToString(), - KeyCombination = dkb.KeyCombination.ToString(), - Action = (int)dkb.Action, - RulesetID = dkb.RulesetID, - Variant = dkb.Variant - }); + realm.Context.Add(new RealmKeyBinding + { + ID = Guid.NewGuid().ToString(), + KeyCombination = dkb.KeyCombination.ToString(), + Action = (int)dkb.Action, + RulesetID = dkb.RulesetID, + Variant = dkb.Variant + }); + } } db.Context.RemoveRange(existingBindings); From 6fd098ca7cb84148d238a40a5c8c666cce0b327d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:18:25 +0900 Subject: [PATCH 021/107] Add full xmldoc to RealmKeyBindingStore --- osu.Game/Input/IKeyBindingStore.cs | 2 +- osu.Game/Input/KeyBindingStore.cs | 2 +- osu.Game/Input/RealmKeyBindingStore.cs | 42 +++++++++++++++++++++----- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs index 50994cb542..ef1d043c33 100644 --- a/osu.Game/Input/IKeyBindingStore.cs +++ b/osu.Game/Input/IKeyBindingStore.cs @@ -13,7 +13,7 @@ namespace osu.Game.Input { event Action KeyBindingChanged; - void Register(KeyBindingContainer manager); + void Register(KeyBindingContainer container); /// /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index ad6bcb4c7c..c55d62b7d6 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -30,7 +30,7 @@ namespace osu.Game.Input } } - public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); /// /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 9910882cef..cd0b85cd8d 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -14,11 +14,15 @@ namespace osu.Game.Input { public class RealmKeyBindingStore : RealmBackedStore, IKeyBindingStore { + /// + /// Fired whenever any key binding change occurs, across all rulesets and types. + /// public event Action KeyBindingChanged; public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) : base(contextFactory, storage) { + // populate defaults from rulesets. using (ContextFactory.GetForWrite()) { foreach (RulesetInfo info in rulesets.AvailableRulesets) @@ -47,11 +51,27 @@ namespace osu.Game.Input } } - public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + /// + /// Register a new type of , adding default bindings from . + /// + /// The container to populate defaults from. + public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); + /// + /// Retrieve all key bindings for the provided specification. + /// + /// An optional ruleset ID. If null, global bindings are returned. + /// An optional ruleset variant. If null, the no-variant bindings are returned. + /// A list of all key bindings found for the query, detached from the database. public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); + /// + /// Retrieve all key bindings for the provided action type. + /// + /// The action to lookup. + /// The enum type of the action. + /// A list of all key bindings found for the query, detached from the database. public List Query(T action) where T : Enum { @@ -60,12 +80,21 @@ namespace osu.Game.Input return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); } + /// + /// Update the database mapping for the provided key binding. + /// + /// The key binding to update. Can be detached from the database. + /// The modification to apply to the key binding. public void Update(IHasGuidPrimaryKey keyBinding, Action modification) { using (var realm = ContextFactory.GetForWrite()) { - var realmKeyBinding = realm.Context.Find(keyBinding.ID); - modification(realmKeyBinding); + RealmKeyBinding realmBinding = keyBinding as RealmKeyBinding; + + if (realmBinding?.IsManaged != true) + realmBinding = realm.Context.Find(keyBinding.ID); + + modification(realmBinding); } KeyBindingChanged?.Invoke(); @@ -101,11 +130,10 @@ namespace osu.Game.Input } /// - /// Retrieve s for a specified ruleset/variant content. + /// Retrieve live queryable s for a specified ruleset/variant content. /// - /// The ruleset's internal ID. - /// An optional variant. - /// + /// An optional ruleset ID. If null, global bindings are returned. + /// An optional ruleset variant. If null, the no-variant bindings are returned. private IQueryable query(int? rulesetId = null, int? variant = null) => ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); } From 6c90f9ceeda882f79526f4760f169a9e61905783 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:22:52 +0900 Subject: [PATCH 022/107] Move RealmWrapper to own file --- osu.Game/Database/RealmContextFactory.cs | 52 -------------------- osu.Game/Database/RealmWrapper.cs | 61 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 52 deletions(-) create mode 100644 osu.Game/Database/RealmWrapper.cs diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 62e2dbba9a..0cdcdc2ef0 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics.CodeAnalysis; using System.Threading; using AutoMapper; using osu.Framework.Platform; @@ -178,57 +177,6 @@ namespace osu.Game.Database } } - [SuppressMessage("ReSharper", "CA2225")] - public class RealmWrapper : IEquatable> - where T : RealmObject, IHasGuidPrimaryKey - { - public Guid ID { get; } - - private readonly ThreadLocal threadValues; - - public readonly IRealmFactory ContextFactory; - - public RealmWrapper(T original, IRealmFactory contextFactory) - { - ContextFactory = contextFactory; - ID = original.Guid; - - var originalContext = original.Realm; - - threadValues = new ThreadLocal(() => - { - var context = ContextFactory?.Get(); - - if (context == null || originalContext?.IsSameInstance(context) != false) - return original; - - return context.Find(ID); - }); - } - - public T Get() => threadValues.Value; - - public RealmWrapper WrapChild(Func lookup) - where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); - - public void PerformUpdate(Action perform) - { - using (ContextFactory.GetForWrite()) - perform(this); - } - - // ReSharper disable once CA2225 - public static implicit operator T(RealmWrapper wrapper) - => wrapper?.Get().Detach(); - - // ReSharper disable once CA2225 - public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); - - public bool Equals(RealmWrapper other) => other != null && other.ID == ID; - - public override string ToString() => Get().ToString(); - } - public static class RealmExtensions { private static readonly IMapper mapper = new MapperConfiguration(c => diff --git a/osu.Game/Database/RealmWrapper.cs b/osu.Game/Database/RealmWrapper.cs new file mode 100644 index 0000000000..06b1bbee90 --- /dev/null +++ b/osu.Game/Database/RealmWrapper.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Realms; + +namespace osu.Game.Database +{ + [SuppressMessage("ReSharper", "CA2225")] + public class RealmWrapper : IEquatable> + where T : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; } + + private readonly ThreadLocal threadValues; + + public readonly IRealmFactory ContextFactory; + + public RealmWrapper(T original, IRealmFactory contextFactory) + { + ContextFactory = contextFactory; + ID = original.Guid; + + var originalContext = original.Realm; + + threadValues = new ThreadLocal(() => + { + var context = ContextFactory?.Get(); + + if (context == null || originalContext?.IsSameInstance(context) != false) + return original; + + return context.Find(ID); + }); + } + + public T Get() => threadValues.Value; + + public RealmWrapper WrapChild(Func lookup) + where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); + + public void PerformUpdate(Action perform) + { + using (ContextFactory.GetForWrite()) + perform(this); + } + + // ReSharper disable once CA2225 + public static implicit operator T(RealmWrapper wrapper) + => wrapper?.Get().Detach(); + + // ReSharper disable once CA2225 + public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); + + public bool Equals(RealmWrapper other) => other != null && other.ID == ID; + + public override string ToString() => Get().ToString(); + } +} From cdb3d20fc62465946a98730452baf6a381da14e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:24:12 +0900 Subject: [PATCH 023/107] Remove unnecessary warning suppression --- osu.Game/Database/RealmWrapper.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Database/RealmWrapper.cs b/osu.Game/Database/RealmWrapper.cs index 06b1bbee90..9792cce527 100644 --- a/osu.Game/Database/RealmWrapper.cs +++ b/osu.Game/Database/RealmWrapper.cs @@ -2,13 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics.CodeAnalysis; using System.Threading; using Realms; namespace osu.Game.Database { - [SuppressMessage("ReSharper", "CA2225")] public class RealmWrapper : IEquatable> where T : RealmObject, IHasGuidPrimaryKey { From 5bb4d359826f2df850a167c502bda25408edd1f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:26:45 +0900 Subject: [PATCH 024/107] Make RealmWrapper nullable enabled --- osu.Game/Database/RealmWrapper.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmWrapper.cs b/osu.Game/Database/RealmWrapper.cs index 9792cce527..a754e208bd 100644 --- a/osu.Game/Database/RealmWrapper.cs +++ b/osu.Game/Database/RealmWrapper.cs @@ -5,6 +5,8 @@ using System; using System.Threading; using Realms; +#nullable enable + namespace osu.Game.Database { public class RealmWrapper : IEquatable> @@ -25,7 +27,7 @@ namespace osu.Game.Database threadValues = new ThreadLocal(() => { - var context = ContextFactory?.Get(); + var context = ContextFactory.Get(); if (context == null || originalContext?.IsSameInstance(context) != false) return original; @@ -42,17 +44,15 @@ namespace osu.Game.Database public void PerformUpdate(Action perform) { using (ContextFactory.GetForWrite()) - perform(this); + perform(Get()); } - // ReSharper disable once CA2225 - public static implicit operator T(RealmWrapper wrapper) + public static implicit operator T?(RealmWrapper? wrapper) => wrapper?.Get().Detach(); - // ReSharper disable once CA2225 public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); - public bool Equals(RealmWrapper other) => other != null && other.ID == ID; + public bool Equals(RealmWrapper? other) => other != null && other.ID == ID; public override string ToString() => Get().ToString(); } From 9f64f6059fd2597bf55f595c4b7b2b44180327a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:30:55 +0900 Subject: [PATCH 025/107] Rename RealmWrapper to Live --- osu.Game/Database/{RealmWrapper.cs => Live.cs} | 14 +++++++------- osu.Game/Database/RealmContextFactory.cs | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Database/{RealmWrapper.cs => Live.cs} (71%) diff --git a/osu.Game/Database/RealmWrapper.cs b/osu.Game/Database/Live.cs similarity index 71% rename from osu.Game/Database/RealmWrapper.cs rename to osu.Game/Database/Live.cs index a754e208bd..3c5e1db157 100644 --- a/osu.Game/Database/RealmWrapper.cs +++ b/osu.Game/Database/Live.cs @@ -9,7 +9,7 @@ using Realms; namespace osu.Game.Database { - public class RealmWrapper : IEquatable> + public class Live : IEquatable> where T : RealmObject, IHasGuidPrimaryKey { public Guid ID { get; } @@ -18,7 +18,7 @@ namespace osu.Game.Database public readonly IRealmFactory ContextFactory; - public RealmWrapper(T original, IRealmFactory contextFactory) + public Live(T original, IRealmFactory contextFactory) { ContextFactory = contextFactory; ID = original.Guid; @@ -38,8 +38,8 @@ namespace osu.Game.Database public T Get() => threadValues.Value; - public RealmWrapper WrapChild(Func lookup) - where TChild : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(lookup(Get()), ContextFactory); + public Live WrapChild(Func lookup) + where TChild : RealmObject, IHasGuidPrimaryKey => new Live(lookup(Get()), ContextFactory); public void PerformUpdate(Action perform) { @@ -47,12 +47,12 @@ namespace osu.Game.Database perform(Get()); } - public static implicit operator T?(RealmWrapper? wrapper) + public static implicit operator T?(Live? wrapper) => wrapper?.Get().Detach(); - public static implicit operator RealmWrapper(T obj) => obj.WrapAsUnmanaged(); + public static implicit operator Live(T obj) => obj.WrapAsUnmanaged(); - public bool Equals(RealmWrapper? other) => other != null && other.ID == ID; + public bool Equals(Live? other) => other != null && other.ID == ID; public override string ToString() => Get().ToString(); } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0cdcdc2ef0..71e9f8c4e1 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -215,10 +215,10 @@ namespace osu.Game.Database return detached; } - public static RealmWrapper Wrap(this T obj, IRealmFactory contextFactory) - where T : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(obj, contextFactory); + public static Live Wrap(this T obj, IRealmFactory contextFactory) + where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); - public static RealmWrapper WrapAsUnmanaged(this T obj) - where T : RealmObject, IHasGuidPrimaryKey => new RealmWrapper(obj, null); + public static Live WrapAsUnmanaged(this T obj) + where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); } } From 20584c9e163cdb9680fd7ec985a3826bb52f6dc8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:50:14 +0900 Subject: [PATCH 026/107] Add full xmldoc for Live class --- osu.Game/Database/Live.cs | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index 3c5e1db157..86cecd51d8 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -9,25 +9,36 @@ using Realms; namespace osu.Game.Database { + /// + /// Provides a method of passing realm live objects across threads in a safe fashion. + /// + /// + /// To consume this as a live instance, the live object should be stored and accessed via each time. + /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. + /// + /// The underlying object type. Should be a with a primary key provided via . public class Live : IEquatable> where T : RealmObject, IHasGuidPrimaryKey { + /// + /// The primary key of the object. + /// public Guid ID { get; } private readonly ThreadLocal threadValues; - public readonly IRealmFactory ContextFactory; + private readonly IRealmFactory contextFactory; public Live(T original, IRealmFactory contextFactory) { - ContextFactory = contextFactory; + this.contextFactory = contextFactory; ID = original.Guid; var originalContext = original.Realm; threadValues = new ThreadLocal(() => { - var context = ContextFactory.Get(); + var context = this.contextFactory.Get(); if (context == null || originalContext?.IsSameInstance(context) != false) return original; @@ -36,14 +47,27 @@ namespace osu.Game.Database }); } + /// + /// Retrieve a live reference to the data. + /// public T Get() => threadValues.Value; + /// + /// Wrap a property of this instance as its own live access object. + /// + /// The child to return. + /// The underlying child object type. Should be a with a primary key provided via . + /// A wrapped instance of the child. public Live WrapChild(Func lookup) - where TChild : RealmObject, IHasGuidPrimaryKey => new Live(lookup(Get()), ContextFactory); + where TChild : RealmObject, IHasGuidPrimaryKey => new Live(lookup(Get()), contextFactory); + /// + /// Perform a write operation on this live object. + /// + /// The action to perform. public void PerformUpdate(Action perform) { - using (ContextFactory.GetForWrite()) + using (contextFactory.GetForWrite()) perform(Get()); } From 05ca016deb04f6e5166f5255d490d82831c7b11e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:57:40 +0900 Subject: [PATCH 027/107] Make Live implement IHasGuidPrimaryKey --- osu.Game/Database/Live.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index 86cecd51d8..7d607a4637 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -17,13 +17,19 @@ namespace osu.Game.Database /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. /// /// The underlying object type. Should be a with a primary key provided via . - public class Live : IEquatable> + public class Live : IEquatable>, IHasGuidPrimaryKey where T : RealmObject, IHasGuidPrimaryKey { /// /// The primary key of the object. /// - public Guid ID { get; } + public Guid Guid { get; } + + public string ID + { + get => Guid.ToString(); + set => throw new NotImplementedException(); + } private readonly ThreadLocal threadValues; @@ -32,7 +38,7 @@ namespace osu.Game.Database public Live(T original, IRealmFactory contextFactory) { this.contextFactory = contextFactory; - ID = original.Guid; + Guid = original.Guid; var originalContext = original.Realm; @@ -43,7 +49,7 @@ namespace osu.Game.Database if (context == null || originalContext?.IsSameInstance(context) != false) return original; - return context.Find(ID); + return context.Find(Guid); }); } @@ -76,7 +82,7 @@ namespace osu.Game.Database public static implicit operator Live(T obj) => obj.WrapAsUnmanaged(); - public bool Equals(Live? other) => other != null && other.ID == ID; + public bool Equals(Live? other) => other != null && other.Guid == Guid; public override string ToString() => Get().ToString(); } From 406e640fa9b6657c1a657ccb6f5425deb2b05015 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 16:59:56 +0900 Subject: [PATCH 028/107] Make key binding update method support all kinds of realm object states --- osu.Game/Input/RealmKeyBindingStore.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index cd0b85cd8d..5b2d2c0dc1 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -87,14 +87,22 @@ namespace osu.Game.Input /// The modification to apply to the key binding. public void Update(IHasGuidPrimaryKey keyBinding, Action modification) { + // the incoming instance could already be a live access object. + Live realmBinding = keyBinding as Live; + using (var realm = ContextFactory.GetForWrite()) { - RealmKeyBinding realmBinding = keyBinding as RealmKeyBinding; + if (realmBinding == null) + { + // the incoming instance could be a raw realm object. + if (!(keyBinding is RealmKeyBinding rkb)) + // if neither of the above cases succeeded, retrieve a realm object for further processing. + rkb = realm.Context.Find(keyBinding.ID); - if (realmBinding?.IsManaged != true) - realmBinding = realm.Context.Find(keyBinding.ID); + realmBinding = new Live(rkb, ContextFactory); + } - modification(realmBinding); + realmBinding.PerformUpdate(modification); } KeyBindingChanged?.Invoke(); From 70689eee2bf1603c3f1755cf46cc29bbb6029788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 17:27:21 +0900 Subject: [PATCH 029/107] Perform initial lookup if original is not managed --- osu.Game/Database/Live.cs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index 7d607a4637..f4882ae325 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -14,7 +14,7 @@ namespace osu.Game.Database /// /// /// To consume this as a live instance, the live object should be stored and accessed via each time. - /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. + /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. /// /// The underlying object type. Should be a with a primary key provided via . public class Live : IEquatable>, IHasGuidPrimaryKey @@ -33,24 +33,34 @@ namespace osu.Game.Database private readonly ThreadLocal threadValues; + private readonly T original; + private readonly IRealmFactory contextFactory; - public Live(T original, IRealmFactory contextFactory) + public Live(T item, IRealmFactory contextFactory) { this.contextFactory = contextFactory; - Guid = original.Guid; - var originalContext = original.Realm; + original = item; + Guid = item.Guid; - threadValues = new ThreadLocal(() => - { - var context = this.contextFactory.Get(); + threadValues = new ThreadLocal(getThreadLocalValue); - if (context == null || originalContext?.IsSameInstance(context) != false) - return original; + // the instance passed in may not be in a managed state. + // for now let's immediately retrieve a managed object on the current thread. + // in the future we may want to delay this until the first access (only populating the Guid at construction time). + if (!item.IsManaged) + original = Get(); + } - return context.Find(Guid); - }); + private T getThreadLocalValue() + { + var context = contextFactory.Get(); + + // only use the original if no context is available or the source realm is the same. + if (context == null || original.Realm?.IsSameInstance(context) == true) return original; + + return context.Find(ID); } /// From a13b6abcff74b5bd9782d8caf7dbf733b7417a59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 18:56:36 +0900 Subject: [PATCH 030/107] Remove incorrect default specification from IRealmFactory interface --- osu.Game/Database/IRealmFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index d65bcaebbe..7b126e10ba 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -7,7 +7,7 @@ namespace osu.Game.Database { public interface IRealmFactory { - public Realm Get() => Realm.GetInstance(); + Realm Get(); /// /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). From 6736db327aa51bce4345e07afe9358eb46731251 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 18:57:56 +0900 Subject: [PATCH 031/107] Remove scheduler being passed in for now --- osu.Game/Database/RealmContextFactory.cs | 10 ++-------- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 71e9f8c4e1..5e8bda65f8 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Threading; using AutoMapper; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; @@ -21,7 +19,6 @@ namespace osu.Game.Database public class RealmContextFactory : IRealmFactory { private readonly Storage storage; - private readonly Scheduler scheduler; private const string database_name = @"client"; @@ -37,10 +34,10 @@ namespace osu.Game.Database private Transaction currentWriteTransaction; - public RealmContextFactory(Storage storage, Scheduler scheduler) + public RealmContextFactory(Storage storage) { this.storage = storage; - this.scheduler = scheduler; + recreateThreadContexts(); using (CreateContext()) @@ -98,9 +95,6 @@ namespace osu.Game.Database return new RealmWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; } - // TODO: remove if not necessary. - public void Schedule(Action action) => scheduler.Add(action); - private Realm getContextForCurrentThread() { var context = threadContexts.Value; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4b64bf2e24..513f44ad5f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -169,7 +169,7 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, Scheduler)); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); dependencies.CacheAs(Storage); From dd50b5870ecb6ef1d09ac3871f4efb7dcd27bf74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 18:58:56 +0900 Subject: [PATCH 032/107] Move extensions methods into own class --- osu.Game/Database/RealmContextFactory.cs | 53 --------------------- osu.Game/Database/RealmExtensions.cs | 60 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 53 deletions(-) create mode 100644 osu.Game/Database/RealmExtensions.cs diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 5e8bda65f8..f78dd65fcc 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,16 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; -using AutoMapper; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Game.Beatmaps; -using osu.Game.Configuration; -using osu.Game.Input.Bindings; -using osu.Game.IO; -using osu.Game.Rulesets; -using osu.Game.Scoring; -using osu.Game.Skinning; using Realms; namespace osu.Game.Database @@ -170,49 +162,4 @@ namespace osu.Game.Database } } } - - public static class RealmExtensions - { - private static readonly IMapper mapper = new MapperConfiguration(c => - { - c.ShouldMapField = fi => false; - c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; - - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - - c.CreateMap() - .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) - .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) - .MaxDepth(2); - - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - }).CreateMapper(); - - public static T Detach(this T obj) where T : RealmObject - { - if (!obj.IsManaged) - return obj; - - var detached = mapper.Map(obj); - - //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); - - return detached; - } - - public static Live Wrap(this T obj, IRealmFactory contextFactory) - where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); - - public static Live WrapAsUnmanaged(this T obj) - where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); - } } diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs new file mode 100644 index 0000000000..8823d75b89 --- /dev/null +++ b/osu.Game/Database/RealmExtensions.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using AutoMapper; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Input.Bindings; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osu.Game.Skinning; +using Realms; + +namespace osu.Game.Database +{ + public static class RealmExtensions + { + private static readonly IMapper mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + + c.CreateMap() + .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) + .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) + .MaxDepth(2); + + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + c.CreateMap(); + }).CreateMapper(); + + public static T Detach(this T obj) where T : RealmObject + { + if (!obj.IsManaged) + return obj; + + var detached = mapper.Map(obj); + + //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); + + return detached; + } + + public static Live Wrap(this T obj, IRealmFactory contextFactory) + where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); + + public static Live WrapAsUnmanaged(this T obj) + where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); + } +} From d810af82eca67d14f62ad3301312489c3c58b3fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 19:28:07 +0900 Subject: [PATCH 033/107] Expose Live.Detach() method for ease of use --- osu.Game/Database/Live.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index f4882ae325..24a2aa258b 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -68,6 +68,11 @@ namespace osu.Game.Database /// public T Get() => threadValues.Value; + /// + /// Retrieve a detached copy of the data. + /// + public T Detach() => Get().Detach(); + /// /// Wrap a property of this instance as its own live access object. /// @@ -88,7 +93,7 @@ namespace osu.Game.Database } public static implicit operator T?(Live? wrapper) - => wrapper?.Get().Detach(); + => wrapper?.Detach() ?? null; public static implicit operator Live(T obj) => obj.WrapAsUnmanaged(); From fc55d67c66899de1a238a95466ce069adc919231 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 19:46:51 +0900 Subject: [PATCH 034/107] Add helper method for detaching lists from realm --- osu.Game/Database/RealmExtensions.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 8823d75b89..99df125f86 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using AutoMapper; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -39,6 +40,16 @@ namespace osu.Game.Database c.CreateMap(); }).CreateMapper(); + public static List Detach(this List items) where T : RealmObject + { + var list = new List(); + + foreach (var obj in items) + list.Add(obj.Detach()); + + return list; + } + public static T Detach(this T obj) where T : RealmObject { if (!obj.IsManaged) From 536e7229d0cb82504a39f0a18e120da91e0b0f12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 19:47:43 +0900 Subject: [PATCH 035/107] Remove unused EF class and unnecessary interface --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 2 +- osu.Game/Input/IKeyBindingStore.cs | 41 ------ osu.Game/Input/KeyBindingStore.cs | 122 ------------------ osu.Game/Input/RealmKeyBindingStore.cs | 9 +- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 2 +- .../KeyBinding/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- 9 files changed, 10 insertions(+), 174 deletions(-) delete mode 100644 osu.Game/Input/IKeyBindingStore.cs delete mode 100644 osu.Game/Input/KeyBindingStore.cs diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index eddaf36f92..bcad8f2d3c 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual typeof(FileStore), typeof(ScoreManager), typeof(BeatmapManager), - typeof(IKeyBindingStore), + typeof(RealmKeyBindingStore), typeof(SettingsStore), typeof(RulesetConfigCache), typeof(OsuColour), diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index ab4854bfd2..62c09440d5 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Input.Bindings private readonly int? variant; [Resolved] - private IKeyBindingStore store { get; set; } + private RealmKeyBindingStore store { get; set; } public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); diff --git a/osu.Game/Input/IKeyBindingStore.cs b/osu.Game/Input/IKeyBindingStore.cs deleted file mode 100644 index ef1d043c33..0000000000 --- a/osu.Game/Input/IKeyBindingStore.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using osu.Framework.Input.Bindings; -using osu.Game.Database; -using osu.Game.Input.Bindings; - -namespace osu.Game.Input -{ - public interface IKeyBindingStore - { - event Action KeyBindingChanged; - - void Register(KeyBindingContainer container); - - /// - /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. - /// - /// The action to lookup. - /// A set of display strings for all the user's key configuration for the action. - IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction); - - /// - /// Retrieve s for a specified ruleset/variant content. - /// - /// The ruleset's internal ID. - /// An optional variant. - /// - List Query(int? rulesetId = null, int? variant = null); - - /// - /// Retrieve s for the specified action. - /// - /// The action to lookup. - List Query(T action) where T : Enum; - - void Update(IHasGuidPrimaryKey keyBinding, Action modification); - } -} diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs deleted file mode 100644 index c55d62b7d6..0000000000 --- a/osu.Game/Input/KeyBindingStore.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Input.Bindings; -using osu.Framework.Platform; -using osu.Game.Database; -using osu.Game.Input.Bindings; -using osu.Game.Rulesets; - -namespace osu.Game.Input -{ - public class KeyBindingStore : DatabaseBackedStore, IKeyBindingStore - { - public event Action KeyBindingChanged; - - public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) - : base(contextFactory, storage) - { - using (ContextFactory.GetForWrite()) - { - foreach (var info in rulesets.AvailableRulesets) - { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); - } - } - } - - public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); - - /// - /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. - /// - /// The action to lookup. - /// A set of display strings for all the user's key configuration for the action. - public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) - { - foreach (var action in query().Where(b => (GlobalAction)b.Action == globalAction)) - { - string str = action.KeyCombination.ReadableString(); - - // even if found, the readable string may be empty for an unbound action. - if (str.Length > 0) - yield return str; - } - } - - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).OfType().ToList(); - - public List Query(T action) where T : Enum - { - int lookup = (int)(object)action; - return query(null, null).Where(rkb => (int)rkb.Action == lookup).OfType().ToList(); - } - - public void Update(IHasGuidPrimaryKey keyBinding, Action modification) - { - throw new NotImplementedException(); - } - - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) - { - using (var usage = ContextFactory.GetForWrite()) - { - // compare counts in database vs defaults - foreach (var group in defaults.GroupBy(k => k.Action)) - { - int count = query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); - int aimCount = group.Count(); - - if (aimCount <= count) - continue; - - foreach (var insertable in group.Skip(count).Take(aimCount - count)) - { - // insert any defaults which are missing. - usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding - { - KeyCombination = insertable.KeyCombination, - Action = insertable.Action, - RulesetID = rulesetId, - Variant = variant - }); - - // required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686) - usage.Context.SaveChanges(); - } - } - } - } - - /// - /// Retrieve s for a specified ruleset/variant content. - /// - /// The ruleset's internal ID. - /// An optional variant. - /// - private List query(int? rulesetId = null, int? variant = null) => - ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); - - public void Update(DatabasedKeyBinding keyBinding) - { - using (ContextFactory.GetForWrite()) - { - var dbKeyBinding = keyBinding; - Refresh(ref dbKeyBinding); - - if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) - return; - - dbKeyBinding.KeyCombination = keyBinding.KeyCombination; - } - - KeyBindingChanged?.Invoke(); - } - } -} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 5b2d2c0dc1..fccd216e4d 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets; namespace osu.Game.Input { - public class RealmKeyBindingStore : RealmBackedStore, IKeyBindingStore + public class RealmKeyBindingStore : RealmBackedStore { /// /// Fired whenever any key binding change occurs, across all rulesets and types. @@ -63,8 +63,7 @@ namespace osu.Game.Input /// An optional ruleset ID. If null, global bindings are returned. /// An optional ruleset variant. If null, the no-variant bindings are returned. /// A list of all key bindings found for the query, detached from the database. - public List Query(int? rulesetId = null, int? variant = null) - => query(rulesetId, variant).ToList().Select(r => r.Detach()).ToList(); + public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).ToList(); /// /// Retrieve all key bindings for the provided action type. @@ -72,12 +71,12 @@ namespace osu.Game.Input /// The action to lookup. /// The enum type of the action. /// A list of all key bindings found for the query, detached from the database. - public List Query(T action) + public List Query(T action) where T : Enum { int lookup = (int)(object)action; - return query(null, null).Where(rkb => rkb.Action == lookup).ToList().Select(r => r.Detach()).ToList(); + return query(null, null).Where(rkb => rkb.Action == lookup).ToList(); } /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 513f44ad5f..07918748df 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -73,7 +73,7 @@ namespace osu.Game protected FileStore FileStore; - protected IKeyBindingStore KeyBindingStore; + protected RealmKeyBindingStore KeyBindingStore; protected SettingsStore SettingsStore; diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 87d51e5268..0a065c9dbc 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.KeyBinding } [Resolved] - private IKeyBindingStore store { get; set; } + private RealmKeyBindingStore store { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index b5d6bc98c3..fbd9a17e2b 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.KeyBinding } [BackgroundDependencyLoader] - private void load(IKeyBindingStore store) + private void load(RealmKeyBindingStore store) { var bindings = store.Query(Ruleset?.ID, variant); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 747f5e9bd0..69e4f734ad 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private IKeyBindingStore keyBindings { get; set; } + private RealmKeyBindingStore keyBindings { get; set; } protected ToolbarButton() : base(HoverSampleSet.Loud) From 8f9b19a76e861f871a653afda37713dc4a6a200c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 19:47:51 +0900 Subject: [PATCH 036/107] Detach at point of usage, rather than point of retrieval --- osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs | 3 ++- osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 62c09440d5..48cab674ca 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; +using osu.Game.Database; using osu.Game.Rulesets; namespace osu.Game.Input.Bindings @@ -63,7 +64,7 @@ namespace osu.Game.Input.Bindings // fallback to defaults instead. KeyBindings = DefaultKeyBindings; else - KeyBindings = store.Query(ruleset?.ID, variant); + KeyBindings = store.Query(ruleset?.ID, variant).Detach(); } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index fbd9a17e2b..bdcbf02ee6 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -6,12 +6,13 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osuTK; -using osu.Game.Graphics; namespace osu.Game.Overlays.KeyBinding { @@ -34,14 +35,14 @@ namespace osu.Game.Overlays.KeyBinding [BackgroundDependencyLoader] private void load(RealmKeyBindingStore store) { - var bindings = store.Query(Ruleset?.ID, variant); + var bindings = store.Query(Ruleset?.ID, variant).Detach(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { int intKey = (int)defaultGroup.Key; // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => ((int)b.Action).Equals(intKey))) + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey))) { AllowMainMouseButtons = Ruleset != null, Defaults = defaultGroup.Select(d => d.KeyCombination) From 0789621b857b30ccc6aee8d5dc151eca2c367451 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 13:51:37 +0900 Subject: [PATCH 037/107] Elaborate on comment mentioning migrations --- osu.Game/Database/RealmContextFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index f78dd65fcc..4325d58d07 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -34,11 +34,11 @@ namespace osu.Game.Database using (CreateContext()) { - // ensure our schema is up-to-date and migrated. + // creating a context will ensure our schema is up-to-date and migrated. } } - private void onMigration(Migration migration, ulong oldschemaversion) + private void onMigration(Migration migration, ulong lastSchemaVersion) { } From ffb42c37dfd644355e3532d5588258029d16fb63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:25:07 +0900 Subject: [PATCH 038/107] Move schema version to const --- osu.Game/Database/RealmContextFactory.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 4325d58d07..243f8c2847 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -14,6 +14,8 @@ namespace osu.Game.Database private const string database_name = @"client"; + private const int schema_version = 5; + private ThreadLocal threadContexts; private readonly object writeLock = new object(); @@ -148,8 +150,8 @@ namespace osu.Game.Database contexts.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { - SchemaVersion = 5, - MigrationCallback = onMigration + SchemaVersion = schema_version, + MigrationCallback = onMigration, }); } From 8cbad1dc1c318db308affd0514a506aa160a148d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:25:22 +0900 Subject: [PATCH 039/107] Add logging of opened and created contexts --- osu.Game/Database/RealmContextFactory.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 243f8c2847..e0fd44ed4a 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -48,7 +48,8 @@ namespace osu.Game.Database private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); - private static readonly GlobalStatistic contexts = GlobalStatistics.Get("Realm", "Contexts"); + private static readonly GlobalStatistic contexts_open = GlobalStatistics.Get("Realm", "Contexts (Open)"); + private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); /// /// Get a context for the current thread for read-only usage. @@ -92,11 +93,16 @@ namespace osu.Game.Database private Realm getContextForCurrentThread() { var context = threadContexts.Value; + if (context?.IsClosed != false) threadContexts.Value = context = CreateContext(); + contexts_open.Value = threadContexts.Values.Count; + if (!refreshCompleted.Value) { + // to keep things simple, realm refreshes are currently performed per thread context at the point of retrieval. + // in the future this should likely be run as part of the update loop for the main (update thread) context. context.Refresh(); refreshCompleted.Value = true; } @@ -147,7 +153,8 @@ namespace osu.Game.Database protected virtual Realm CreateContext() { - contexts.Value++; + contexts_created.Value++; + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) { SchemaVersion = schema_version, From 0dca9c8c464eae0440c4426401d76e52897e8afa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:36:35 +0900 Subject: [PATCH 040/107] Tidy up RealmContextFactory; remove delete/dispose method which wouldn't work due to threading --- osu.Game/Database/RealmContextFactory.cs | 52 +++++++++--------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index e0fd44ed4a..e11379869a 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; using Realms; @@ -16,8 +17,11 @@ namespace osu.Game.Database private const int schema_version = 5; - private ThreadLocal threadContexts; + private readonly ThreadLocal threadContexts; + /// + /// Lock object which is held for the duration of a write operation (via ). + /// private readonly object writeLock = new object(); private ThreadLocal refreshCompleted = new ThreadLocal(); @@ -32,10 +36,11 @@ namespace osu.Game.Database { this.storage = storage; - recreateThreadContexts(); + threadContexts = new ThreadLocal(createContext, true); - using (CreateContext()) + using (var realm = Get()) { + Logger.Log($"Opened realm {database_name} at version {realm.Config.SchemaVersion}"); // creating a context will ensure our schema is up-to-date and migrated. } } @@ -95,7 +100,7 @@ namespace osu.Game.Database var context = threadContexts.Value; if (context?.IsClosed != false) - threadContexts.Value = context = CreateContext(); + threadContexts.Value = context = createContext(); contexts_open.Value = threadContexts.Values.Count; @@ -110,6 +115,17 @@ namespace osu.Game.Database return context; } + private Realm createContext() + { + contexts_created.Value++; + + return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) + { + SchemaVersion = schema_version, + MigrationCallback = onMigration, + }); + } + private void usageCompleted(RealmWriteUsage usage) { int usages = Interlocked.Decrement(ref currentWriteUsages); @@ -142,33 +158,5 @@ namespace osu.Game.Database Monitor.Exit(writeLock); } } - - private void recreateThreadContexts() - { - // Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed - // for other threads to use, and we rely on the finalizer inside OsuDbContext to handle their previous contexts - threadContexts?.Value.Dispose(); - threadContexts = new ThreadLocal(CreateContext, true); - } - - protected virtual Realm CreateContext() - { - contexts_created.Value++; - - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) - { - SchemaVersion = schema_version, - MigrationCallback = onMigration, - }); - } - - public void ResetDatabase() - { - lock (writeLock) - { - recreateThreadContexts(); - storage.DeleteDatabase(database_name); - } - } } } From 2e4c3c8e3941a15eaa2d7a3703d2ebe1c9b3fdd6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:42:43 +0900 Subject: [PATCH 041/107] Avoid closing initial context after migrations (unnecessary) --- osu.Game/Database/RealmContextFactory.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index e11379869a..fa0fecc90c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -38,11 +38,9 @@ namespace osu.Game.Database threadContexts = new ThreadLocal(createContext, true); - using (var realm = Get()) - { - Logger.Log($"Opened realm {database_name} at version {realm.Config.SchemaVersion}"); - // creating a context will ensure our schema is up-to-date and migrated. - } + // creating a context will ensure our schema is up-to-date and migrated. + var realm = Get(); + Logger.Log($"Opened realm \"{realm.Config.DatabasePath}\" at version {realm.Config.SchemaVersion}"); } private void onMigration(Migration migration, ulong lastSchemaVersion) From ff16d2f490dc7836dd96728e4ffdb81b20db185c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:55:45 +0900 Subject: [PATCH 042/107] Mark classes nullable --- osu.Game/Database/RealmBackedStore.cs | 6 ++++-- osu.Game/Input/RealmKeyBindingStore.cs | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/RealmBackedStore.cs b/osu.Game/Database/RealmBackedStore.cs index e37831d9d5..4e58ef773b 100644 --- a/osu.Game/Database/RealmBackedStore.cs +++ b/osu.Game/Database/RealmBackedStore.cs @@ -3,15 +3,17 @@ using osu.Framework.Platform; +#nullable enable + namespace osu.Game.Database { public abstract class RealmBackedStore { - protected readonly Storage Storage; + protected readonly Storage? Storage; protected readonly IRealmFactory ContextFactory; - protected RealmBackedStore(IRealmFactory contextFactory, Storage storage = null) + protected RealmBackedStore(IRealmFactory contextFactory, Storage? storage = null) { ContextFactory = contextFactory; Storage = storage; diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index fccd216e4d..95751306f3 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -10,6 +10,8 @@ using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Rulesets; +#nullable enable + namespace osu.Game.Input { public class RealmKeyBindingStore : RealmBackedStore @@ -17,19 +19,22 @@ namespace osu.Game.Input /// /// Fired whenever any key binding change occurs, across all rulesets and types. /// - public event Action KeyBindingChanged; + public event Action? KeyBindingChanged; - public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) + public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore? rulesets, Storage? storage = null) : base(contextFactory, storage) { - // populate defaults from rulesets. - using (ContextFactory.GetForWrite()) + if (rulesets != null) { - foreach (RulesetInfo info in rulesets.AvailableRulesets) + // populate defaults from rulesets. + using (ContextFactory.GetForWrite()) { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + foreach (RulesetInfo info in rulesets.AvailableRulesets) + { + var ruleset = info.CreateInstance(); + foreach (var variant in ruleset.AvailableVariants) + insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + } } } } @@ -87,7 +92,7 @@ namespace osu.Game.Input public void Update(IHasGuidPrimaryKey keyBinding, Action modification) { // the incoming instance could already be a live access object. - Live realmBinding = keyBinding as Live; + Live? realmBinding = keyBinding as Live; using (var realm = ContextFactory.GetForWrite()) { From a6997a6fc67d44754c59d6c71d112baddefe81a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 14:59:48 +0900 Subject: [PATCH 043/107] Move ruleset key binding registration to an explicit method rather than the constructor --- osu.Game/Input/RealmKeyBindingStore.cs | 30 ++++++++++++++------------ osu.Game/OsuGameBase.cs | 6 +++++- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 95751306f3..8b962e2e9a 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -21,22 +21,9 @@ namespace osu.Game.Input /// public event Action? KeyBindingChanged; - public RealmKeyBindingStore(RealmContextFactory contextFactory, RulesetStore? rulesets, Storage? storage = null) + public RealmKeyBindingStore(RealmContextFactory contextFactory, Storage? storage = null) : base(contextFactory, storage) { - if (rulesets != null) - { - // populate defaults from rulesets. - using (ContextFactory.GetForWrite()) - { - foreach (RulesetInfo info in rulesets.AvailableRulesets) - { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); - } - } - } } /// @@ -62,6 +49,21 @@ namespace osu.Game.Input /// The container to populate defaults from. public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings); + /// + /// Register a ruleset, adding default bindings for each of its variants. + /// + /// The ruleset to populate defaults from. + public void Register(RulesetInfo ruleset) + { + var instance = ruleset.CreateInstance(); + + using (ContextFactory.GetForWrite()) + { + foreach (var variant in instance.AvailableVariants) + insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); + } + } + /// /// Retrieve all key bindings for the provided specification. /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 07918748df..65eca8255e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -267,7 +267,7 @@ namespace osu.Game migrateDataToRealm(); - dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory, RulesetStore)); + dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); @@ -310,6 +310,10 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChild(MenuCursorContainer)); KeyBindingStore.Register(globalBindings); + + foreach (var r in RulesetStore.AvailableRulesets) + KeyBindingStore.Register(r); + dependencies.Cache(globalBindings); PreviewTrackManager previewTrackManager; From 9a5410e5d235431cc8f2394cd1d1567f9a38a676 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 15:19:50 +0900 Subject: [PATCH 044/107] Add basic test coverage --- .../Database/TestRealmKeyBindingStore.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 osu.Game.Tests/Database/TestRealmKeyBindingStore.cs diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs new file mode 100644 index 0000000000..d8eb3a9906 --- /dev/null +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osuTK.Input; + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class TestRealmKeyBindingStore + { + private NativeStorage storage; + + private RealmKeyBindingStore keyBindingStore; + + private RealmContextFactory realmContextFactory; + + [SetUp] + public void SetUp() + { + var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + + storage = new NativeStorage(directory.FullName); + + realmContextFactory = new RealmContextFactory(storage); + keyBindingStore = new RealmKeyBindingStore(realmContextFactory); + } + + [Test] + public void TestDefaultsPopulationAndQuery() + { + Assert.That(keyBindingStore.Query().Count, Is.EqualTo(0)); + + KeyBindingContainer testContainer = new TestKeyBindingContainer(); + + keyBindingStore.Register(testContainer); + + Assert.That(keyBindingStore.Query().Count, Is.EqualTo(3)); + + Assert.That(keyBindingStore.Query(GlobalAction.Back).Count, Is.EqualTo(1)); + Assert.That(keyBindingStore.Query(GlobalAction.Select).Count, Is.EqualTo(2)); + } + + [Test] + public void TestUpdateViaQueriedReference() + { + KeyBindingContainer testContainer = new TestKeyBindingContainer(); + + keyBindingStore.Register(testContainer); + + var backBinding = keyBindingStore.Query(GlobalAction.Back).Single(); + + Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); + + keyBindingStore.Update(backBinding, binding => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); + + Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); + + // check still correct after re-query. + backBinding = keyBindingStore.Query(GlobalAction.Back).Single(); + Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); + } + + [TearDown] + public void TearDown() + { + storage.DeleteDirectory(string.Empty); + } + + public class TestKeyBindingContainer : KeyBindingContainer + { + public override IEnumerable DefaultKeyBindings => + new[] + { + new KeyBinding(InputKey.Escape, GlobalAction.Back), + new KeyBinding(InputKey.Enter, GlobalAction.Select), + new KeyBinding(InputKey.Space, GlobalAction.Select), + }; + } + } +} From 7769d95e7b62a11047a053b7bdeee8cc84e1ecfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 15:48:26 +0900 Subject: [PATCH 045/107] Add xmldoc for extension methods --- osu.Game/Database/RealmExtensions.cs | 53 +++++++++++++++++++++------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 99df125f86..e7dd335ea3 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using AutoMapper; using osu.Game.Beatmaps; @@ -40,6 +41,12 @@ namespace osu.Game.Database c.CreateMap(); }).CreateMapper(); + /// + /// Create a detached copy of the each item in the list. + /// + /// A list of managed s to detach. + /// The type of object. + /// A list containing non-managed copies of provided items. public static List Detach(this List items) where T : RealmObject { var list = new List(); @@ -50,22 +57,44 @@ namespace osu.Game.Database return list; } - public static T Detach(this T obj) where T : RealmObject + /// + /// Create a detached copy of the each item in the list. + /// + /// The managed to detach. + /// The type of object. + /// A non-managed copy of provided item. Will return the provided item if already detached. + public static T Detach(this T item) where T : RealmObject { - if (!obj.IsManaged) - return obj; + if (!item.IsManaged) + return item; - var detached = mapper.Map(obj); - - //typeof(RealmObject).GetField("_realm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.SetValue(detached, null); - - return detached; + return mapper.Map(item); } - public static Live Wrap(this T obj, IRealmFactory contextFactory) - where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, contextFactory); + /// + /// Wrap a managed instance of a realm object in a . + /// + /// The item to wrap. + /// A factory to retrieve realm contexts from. + /// The type of object. + /// A wrapped instance of the provided item. + public static Live Wrap(this T item, IRealmFactory contextFactory) + where T : RealmObject, IHasGuidPrimaryKey => new Live(item, contextFactory); - public static Live WrapAsUnmanaged(this T obj) - where T : RealmObject, IHasGuidPrimaryKey => new Live(obj, null); + /// + /// Wrap an unmanaged instance of a realm object in a . + /// + /// The item to wrap. + /// The type of object. + /// A wrapped instance of the provided item. + /// Throws if the provided item is managed. + public static Live WrapAsUnmanaged(this T item) + where T : RealmObject, IHasGuidPrimaryKey + { + if (item.IsManaged) + throw new ArgumentException("Provided item must not be managed", nameof(item)); + + return new Live(item, null); + } } } From f0a9688baa5be4962c8908fd5adb46b8f1ec78d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 15:50:09 +0900 Subject: [PATCH 046/107] Remove unnecessary mapped type --- osu.Game/Database/RealmExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index e7dd335ea3..7b63395192 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -32,7 +32,6 @@ namespace osu.Game.Database .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) .MaxDepth(2); - c.CreateMap(); c.CreateMap(); c.CreateMap(); c.CreateMap(); From 46a1d99c742c58502da7a1ffbbee7eef94f3c2e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 17:01:16 +0900 Subject: [PATCH 047/107] Allow detach to be run against an IQueryable directly --- osu.Game/Database/RealmExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 7b63395192..b25299bf23 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -41,12 +41,12 @@ namespace osu.Game.Database }).CreateMapper(); /// - /// Create a detached copy of the each item in the list. + /// Create a detached copy of the each item in the collection. /// /// A list of managed s to detach. /// The type of object. /// A list containing non-managed copies of provided items. - public static List Detach(this List items) where T : RealmObject + public static List Detach(this IEnumerable items) where T : RealmObject { var list = new List(); From 765d9cfae1c38e09fba32e3ab667f6f40651d63f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 17:01:40 +0900 Subject: [PATCH 048/107] Use direct access for query pattern --- .../Database/TestRealmKeyBindingStore.cs | 2 -- .../Bindings/DatabasedKeyBindingContainer.cs | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index d8eb3a9906..3402b95739 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -6,13 +6,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NUnit.Framework; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; -using osuTK.Input; namespace osu.Game.Tests.Database { diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 48cab674ca..03da76b330 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Database; @@ -24,6 +25,9 @@ namespace osu.Game.Input.Bindings [Resolved] private RealmKeyBindingStore store { get; set; } + [Resolved] + private RealmContextFactory realmFactory { get; set; } + public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); /// @@ -64,7 +68,16 @@ namespace osu.Game.Input.Bindings // fallback to defaults instead. KeyBindings = DefaultKeyBindings; else - KeyBindings = store.Query(ruleset?.ID, variant).Detach(); + { + var rulesetId = ruleset?.ID; + + // #1 + KeyBindings = store.Query(rulesetId, variant).Detach(); + + // #2 (Clearly shows lifetime of realm context access) + using (var realm = realmFactory.Get()) + KeyBindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + } } } } From 192e58e0c6c447cb0ae93c641e1e19396c7a22a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 16:53:04 +0900 Subject: [PATCH 049/107] Update all read queries to use direct realm subscriptions/queries --- .../Database/TestRealmKeyBindingStore.cs | 14 +++-- osu.Game/Database/RealmBackedStore.cs | 6 +- osu.Game/Database/RealmContextFactory.cs | 2 +- .../Bindings/DatabasedKeyBindingContainer.cs | 55 +++++++++++-------- osu.Game/Input/RealmKeyBindingStore.cs | 36 +++--------- .../KeyBinding/KeyBindingsSubsection.cs | 25 +++++---- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 52 ++++++++++-------- 7 files changed, 97 insertions(+), 93 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 3402b95739..58633e2f03 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -37,18 +37,20 @@ namespace osu.Game.Tests.Database [Test] public void TestDefaultsPopulationAndQuery() { - Assert.That(keyBindingStore.Query().Count, Is.EqualTo(0)); + Assert.That(query().Count, Is.EqualTo(0)); KeyBindingContainer testContainer = new TestKeyBindingContainer(); keyBindingStore.Register(testContainer); - Assert.That(keyBindingStore.Query().Count, Is.EqualTo(3)); + Assert.That(query().Count, Is.EqualTo(3)); - Assert.That(keyBindingStore.Query(GlobalAction.Back).Count, Is.EqualTo(1)); - Assert.That(keyBindingStore.Query(GlobalAction.Select).Count, Is.EqualTo(2)); + Assert.That(query().Where(k => k.Action == (int)GlobalAction.Back).Count, Is.EqualTo(1)); + Assert.That(query().Where(k => k.Action == (int)GlobalAction.Select).Count, Is.EqualTo(2)); } + private IQueryable query() => realmContextFactory.Get().All(); + [Test] public void TestUpdateViaQueriedReference() { @@ -56,7 +58,7 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer); - var backBinding = keyBindingStore.Query(GlobalAction.Back).Single(); + var backBinding = query().Single(k => k.Action == (int)GlobalAction.Back); Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); @@ -65,7 +67,7 @@ namespace osu.Game.Tests.Database Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); // check still correct after re-query. - backBinding = keyBindingStore.Query(GlobalAction.Back).Single(); + backBinding = query().Single(k => k.Action == (int)GlobalAction.Back); Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); } diff --git a/osu.Game/Database/RealmBackedStore.cs b/osu.Game/Database/RealmBackedStore.cs index 4e58ef773b..bc67e332fe 100644 --- a/osu.Game/Database/RealmBackedStore.cs +++ b/osu.Game/Database/RealmBackedStore.cs @@ -11,11 +11,11 @@ namespace osu.Game.Database { protected readonly Storage? Storage; - protected readonly IRealmFactory ContextFactory; + protected readonly IRealmFactory RealmFactory; - protected RealmBackedStore(IRealmFactory contextFactory, Storage? storage = null) + protected RealmBackedStore(IRealmFactory realmFactory, Storage? storage = null) { - ContextFactory = contextFactory; + RealmFactory = realmFactory; Storage = storage; } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index fa0fecc90c..b6eb28aa33 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -77,7 +77,7 @@ namespace osu.Game.Database try { - context = getContextForCurrentThread(); + context = createContext(); currentWriteTransaction ??= context.BeginWrite(); } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 03da76b330..d5ae4d9bd6 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Database; using osu.Game.Rulesets; +using Realms; namespace osu.Game.Input.Bindings { @@ -22,6 +23,9 @@ namespace osu.Game.Input.Bindings private readonly int? variant; + private IDisposable realmSubscription; + private IQueryable realmKeyBindings; + [Resolved] private RealmKeyBindingStore store { get; set; } @@ -49,35 +53,42 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { + var realm = realmFactory.Get(); + + if (ruleset == null || ruleset.ID.HasValue) + { + var rulesetId = ruleset?.ID; + + realmKeyBindings = realm.All() + .Where(b => b.RulesetID == rulesetId && b.Variant == variant); + + realmSubscription = realmKeyBindings + .SubscribeForNotifications((sender, changes, error) => + { + // first subscription ignored as we are handling this in LoadComplete. + if (changes == null) + return; + + ReloadMappings(); + }); + } + base.LoadComplete(); - store.KeyBindingChanged += ReloadMappings; + } + + protected override void ReloadMappings() + { + if (realmKeyBindings != null) + KeyBindings = realmKeyBindings.Detach(); + else + base.ReloadMappings(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (store != null) - store.KeyBindingChanged -= ReloadMappings; - } - - protected override void ReloadMappings() - { - if (ruleset != null && !ruleset.ID.HasValue) - // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings. - // fallback to defaults instead. - KeyBindings = DefaultKeyBindings; - else - { - var rulesetId = ruleset?.ID; - - // #1 - KeyBindings = store.Query(rulesetId, variant).Detach(); - - // #2 (Clearly shows lifetime of realm context access) - using (var realm = realmFactory.Get()) - KeyBindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); - } + realmSubscription?.Dispose(); } } } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 8b962e2e9a..0af1beefb7 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -21,8 +21,8 @@ namespace osu.Game.Input /// public event Action? KeyBindingChanged; - public RealmKeyBindingStore(RealmContextFactory contextFactory, Storage? storage = null) - : base(contextFactory, storage) + public RealmKeyBindingStore(RealmContextFactory realmFactory, Storage? storage = null) + : base(realmFactory, storage) { } @@ -57,35 +57,13 @@ namespace osu.Game.Input { var instance = ruleset.CreateInstance(); - using (ContextFactory.GetForWrite()) + using (RealmFactory.GetForWrite()) { foreach (var variant in instance.AvailableVariants) insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); } } - /// - /// Retrieve all key bindings for the provided specification. - /// - /// An optional ruleset ID. If null, global bindings are returned. - /// An optional ruleset variant. If null, the no-variant bindings are returned. - /// A list of all key bindings found for the query, detached from the database. - public List Query(int? rulesetId = null, int? variant = null) => query(rulesetId, variant).ToList(); - - /// - /// Retrieve all key bindings for the provided action type. - /// - /// The action to lookup. - /// The enum type of the action. - /// A list of all key bindings found for the query, detached from the database. - public List Query(T action) - where T : Enum - { - int lookup = (int)(object)action; - - return query(null, null).Where(rkb => rkb.Action == lookup).ToList(); - } - /// /// Update the database mapping for the provided key binding. /// @@ -96,7 +74,7 @@ namespace osu.Game.Input // the incoming instance could already be a live access object. Live? realmBinding = keyBinding as Live; - using (var realm = ContextFactory.GetForWrite()) + using (var realm = RealmFactory.GetForWrite()) { if (realmBinding == null) { @@ -105,7 +83,7 @@ namespace osu.Game.Input // if neither of the above cases succeeded, retrieve a realm object for further processing. rkb = realm.Context.Find(keyBinding.ID); - realmBinding = new Live(rkb, ContextFactory); + realmBinding = new Live(rkb, RealmFactory); } realmBinding.PerformUpdate(modification); @@ -116,7 +94,7 @@ namespace osu.Game.Input private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { - using (var usage = ContextFactory.GetForWrite()) + using (var usage = RealmFactory.GetForWrite()) { // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) @@ -149,6 +127,6 @@ namespace osu.Game.Input /// An optional ruleset ID. If null, global bindings are returned. /// An optional ruleset variant. If null, the no-variant bindings are returned. private IQueryable query(int? rulesetId = null, int? variant = null) => - ContextFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); + RealmFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index bdcbf02ee6..b067e50d4d 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -using osu.Game.Input; +using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osuTK; @@ -33,20 +33,25 @@ namespace osu.Game.Overlays.KeyBinding } [BackgroundDependencyLoader] - private void load(RealmKeyBindingStore store) + private void load(RealmContextFactory realmFactory) { - var bindings = store.Query(Ruleset?.ID, variant).Detach(); + var rulesetId = Ruleset?.ID; - foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) + using (var realm = realmFactory.Get()) { - int intKey = (int)defaultGroup.Key; + var bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); - // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey))) + foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { - AllowMainMouseButtons = Ruleset != null, - Defaults = defaultGroup.Select(d => d.KeyCombination) - }); + int intKey = (int)defaultGroup.Key; + + // one row per valid action. + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey))) + { + AllowMainMouseButtons = Ruleset != null, + Defaults = defaultGroup.Select(d => d.KeyCombination) + }); + } } Add(new ResetButton diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 69e4f734ad..fcb5031657 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,12 +12,12 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Input; using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; @@ -75,7 +74,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private RealmKeyBindingStore keyBindings { get; set; } + private RealmContextFactory realmFactory { get; set; } protected ToolbarButton() : base(HoverSampleSet.Loud) @@ -158,32 +157,28 @@ namespace osu.Game.Overlays.Toolbar }; } - private readonly Cached tooltipKeyBinding = new Cached(); + private RealmKeyBinding realmKeyBinding; - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { - keyBindings.KeyBindingChanged += () => tooltipKeyBinding.Invalidate(); - updateKeyBindingTooltip(); - } - - private void updateKeyBindingTooltip() - { - if (tooltipKeyBinding.IsValid) - return; - - keyBindingTooltip.Text = string.Empty; + base.LoadComplete(); if (Hotkey != null) { - KeyCombination? binding = keyBindings.Query(Hotkey.Value).FirstOrDefault()?.KeyCombination; - var keyBindingString = binding?.ReadableString(); + var realm = realmFactory.Get(); + realmKeyBinding = realm.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.Action == (int)Hotkey.Value); - if (!string.IsNullOrEmpty(keyBindingString)) - keyBindingTooltip.Text = $" ({keyBindingString})"; + if (realmKeyBinding != null) + { + realmKeyBinding.PropertyChanged += (sender, args) => + { + if (args.PropertyName == nameof(realmKeyBinding.KeyCombination)) + updateKeyBindingTooltip(); + }; + } + + updateKeyBindingTooltip(); } - - tooltipKeyBinding.Validate(); } protected override bool OnMouseDown(MouseDownEvent e) => true; @@ -224,6 +219,19 @@ namespace osu.Game.Overlays.Toolbar public void OnReleased(GlobalAction action) { } + + private void updateKeyBindingTooltip() + { + if (realmKeyBinding != null) + { + KeyCombination? binding = ((IKeyBinding)realmKeyBinding).KeyCombination; + + var keyBindingString = binding?.ReadableString(); + + if (!string.IsNullOrEmpty(keyBindingString)) + keyBindingTooltip.Text = $" ({keyBindingString})"; + } + } } public class OpaqueBackground : Container From 78707c3b0615d7315289a3a68262fa456017b21e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 17:03:02 +0900 Subject: [PATCH 050/107] Remove unused event --- osu.Game/Input/RealmKeyBindingStore.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 0af1beefb7..b42d2688f0 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -16,11 +16,6 @@ namespace osu.Game.Input { public class RealmKeyBindingStore : RealmBackedStore { - /// - /// Fired whenever any key binding change occurs, across all rulesets and types. - /// - public event Action? KeyBindingChanged; - public RealmKeyBindingStore(RealmContextFactory realmFactory, Storage? storage = null) : base(realmFactory, storage) { @@ -88,8 +83,6 @@ namespace osu.Game.Input realmBinding.PerformUpdate(modification); } - - KeyBindingChanged?.Invoke(); } private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) From 542f535247d5d3a4c90570cbbcf0b273a6f7231d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 17:34:44 +0900 Subject: [PATCH 051/107] Pull out thread local contexts and have main realm refresh in update loop --- osu.Game/Database/IRealmFactory.cs | 12 ++- osu.Game/Database/RealmContextFactory.cs | 93 +++++++++++------------- osu.Game/OsuGameBase.cs | 1 + 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 7b126e10ba..0fffc3d7be 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -7,10 +7,18 @@ namespace osu.Game.Database { public interface IRealmFactory { - Realm Get(); + /// + /// The main realm context, bound to the update thread. + /// + public Realm Context { get; } /// - /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// Get a fresh context for read usage. + /// + Realm GetForRead(); + + /// + /// Request a context for write usage. /// This method may block if a write is already active on a different thread. /// /// A usage containing a usable context. diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index b6eb28aa33..ea3549a9cd 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; @@ -9,16 +10,13 @@ using Realms; namespace osu.Game.Database { - public class RealmContextFactory : IRealmFactory + public class RealmContextFactory : Component, IRealmFactory { private readonly Storage storage; - private const string database_name = @"client"; private const int schema_version = 5; - private readonly ThreadLocal threadContexts; - /// /// Lock object which is held for the duration of a write operation (via ). /// @@ -32,54 +30,55 @@ namespace osu.Game.Database private Transaction currentWriteTransaction; - public RealmContextFactory(Storage storage) - { - this.storage = storage; - - threadContexts = new ThreadLocal(createContext, true); - - // creating a context will ensure our schema is up-to-date and migrated. - var realm = Get(); - Logger.Log($"Opened realm \"{realm.Config.DatabasePath}\" at version {realm.Config.SchemaVersion}"); - } - - private void onMigration(Migration migration, ulong lastSchemaVersion) - { - } - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); + private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Refreshes"); private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); private static readonly GlobalStatistic contexts_open = GlobalStatistics.Get("Realm", "Contexts (Open)"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); - /// - /// Get a context for the current thread for read-only usage. - /// If a is in progress, the existing write-safe context will be returned. - /// - public Realm Get() + private Realm context; + + public Realm Context { - reads.Value++; - return getContextForCurrentThread(); + get + { + if (context == null) + { + context = createContext(); + Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); + } + + // creating a context will ensure our schema is up-to-date and migrated. + + return context; + } + } + + public RealmContextFactory(Storage storage) + { + this.storage = storage; + } + + public Realm GetForRead() + { + reads.Value++; + return createContext(); } - /// - /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). - /// This method may block if a write is already active on a different thread. - /// - /// A usage containing a usable context. public RealmWriteUsage GetForWrite() { writes.Value++; Monitor.Enter(writeLock); - Realm context; + + Realm realm; try { - context = createContext(); + realm = createContext(); - currentWriteTransaction ??= context.BeginWrite(); + currentWriteTransaction ??= realm.BeginWrite(); } catch { @@ -90,27 +89,15 @@ namespace osu.Game.Database Interlocked.Increment(ref currentWriteUsages); - return new RealmWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; + return new RealmWriteUsage(realm, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; } - private Realm getContextForCurrentThread() + protected override void Update() { - var context = threadContexts.Value; + base.Update(); - if (context?.IsClosed != false) - threadContexts.Value = context = createContext(); - - contexts_open.Value = threadContexts.Values.Count; - - if (!refreshCompleted.Value) - { - // to keep things simple, realm refreshes are currently performed per thread context at the point of retrieval. - // in the future this should likely be run as part of the update loop for the main (update thread) context. - context.Refresh(); - refreshCompleted.Value = true; - } - - return context; + if (Context.Refresh()) + refreshes.Value++; } private Realm createContext() @@ -156,5 +143,9 @@ namespace osu.Game.Database Monitor.Exit(writeLock); } } + + private void onMigration(Migration migration, ulong lastSchemaVersion) + { + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 65eca8255e..01f161cfd9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -170,6 +170,7 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); + AddInternal(realmFactory); dependencies.CacheAs(Storage); From 9d744d629f28f85ba8231aa51ff38de441e3fb18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 17:35:00 +0900 Subject: [PATCH 052/107] Update existing usages to use the main realm context where applicable --- osu.Game.Tests/Database/TestRealmKeyBindingStore.cs | 2 +- osu.Game/Database/Live.cs | 2 +- .../Input/Bindings/DatabasedKeyBindingContainer.cs | 6 ++---- osu.Game/Input/RealmKeyBindingStore.cs | 12 ++---------- .../Overlays/KeyBinding/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 3 +-- 6 files changed, 8 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 58633e2f03..691c55c601 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Database Assert.That(query().Where(k => k.Action == (int)GlobalAction.Select).Count, Is.EqualTo(2)); } - private IQueryable query() => realmContextFactory.Get().All(); + private IQueryable query() => realmContextFactory.Context.All(); [Test] public void TestUpdateViaQueriedReference() diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs index 24a2aa258b..49218ddd6e 100644 --- a/osu.Game/Database/Live.cs +++ b/osu.Game/Database/Live.cs @@ -55,7 +55,7 @@ namespace osu.Game.Database private T getThreadLocalValue() { - var context = contextFactory.Get(); + var context = contextFactory.Context; // only use the original if no context is available or the source realm is the same. if (context == null || original.Realm?.IsSameInstance(context) == true) return original; diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index d5ae4d9bd6..04f050f536 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -53,14 +53,12 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - var realm = realmFactory.Get(); - if (ruleset == null || ruleset.ID.HasValue) { var rulesetId = ruleset?.ID; - realmKeyBindings = realm.All() - .Where(b => b.RulesetID == rulesetId && b.Variant == variant); + realmKeyBindings = realmFactory.Context.All() + .Where(b => b.RulesetID == rulesetId && b.Variant == variant); realmSubscription = realmKeyBindings .SubscribeForNotifications((sender, changes, error) => diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index b42d2688f0..dd487af9bc 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -28,7 +28,7 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in query().Where(b => (GlobalAction)b.Action == globalAction)) + foreach (var action in RealmFactory.Context.All().Where(b => (GlobalAction)b.Action == globalAction)) { string str = ((IKeyBinding)action).KeyCombination.ReadableString(); @@ -92,7 +92,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = query(rulesetId, variant).Count(k => k.Action == (int)group.Key); + int count = usage.Context.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.Action == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -113,13 +113,5 @@ namespace osu.Game.Input } } } - - /// - /// Retrieve live queryable s for a specified ruleset/variant content. - /// - /// An optional ruleset ID. If null, global bindings are returned. - /// An optional ruleset variant. If null, the no-variant bindings are returned. - private IQueryable query(int? rulesetId = null, int? variant = null) => - RealmFactory.Get().All().Where(b => b.RulesetID == rulesetId && b.Variant == variant); } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index b067e50d4d..0f95d07da8 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.KeyBinding { var rulesetId = Ruleset?.ID; - using (var realm = realmFactory.Get()) + using (var realm = realmFactory.GetForRead()) { var bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index fcb5031657..7bb0eb894c 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -165,8 +165,7 @@ namespace osu.Game.Overlays.Toolbar if (Hotkey != null) { - var realm = realmFactory.Get(); - realmKeyBinding = realm.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.Action == (int)Hotkey.Value); + realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.Action == (int)Hotkey.Value); if (realmKeyBinding != null) { From 9086d7554270658fc70a12c0327f77c180bc05aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 17:59:47 +0900 Subject: [PATCH 053/107] Update write usages --- .../Database/TestRealmKeyBindingStore.cs | 7 +++- osu.Game/Input/RealmKeyBindingStore.cs | 26 ------------- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 39 +++++++++++++------ .../KeyBinding/KeyBindingsSubsection.cs | 2 +- 4 files changed, 34 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 691c55c601..426593f5de 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -62,7 +62,12 @@ namespace osu.Game.Tests.Database Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); - keyBindingStore.Update(backBinding, binding => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); + var binding = backBinding; + + realmContextFactory.Context.Write(() => + { + ((IKeyBinding)binding).KeyCombination = new KeyCombination(InputKey.BackSpace); + }); Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index dd487af9bc..478f4792f8 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -59,32 +59,6 @@ namespace osu.Game.Input } } - /// - /// Update the database mapping for the provided key binding. - /// - /// The key binding to update. Can be detached from the database. - /// The modification to apply to the key binding. - public void Update(IHasGuidPrimaryKey keyBinding, Action modification) - { - // the incoming instance could already be a live access object. - Live? realmBinding = keyBinding as Live; - - using (var realm = RealmFactory.GetForWrite()) - { - if (realmBinding == null) - { - // the incoming instance could be a raw realm object. - if (!(keyBinding is RealmKeyBinding rkb)) - // if neither of the above cases succeeded, retrieve a realm object for further processing. - rkb = realm.Context.Find(keyBinding.ID); - - realmBinding = new Live(rkb, RealmFactory); - } - - realmBinding.PerformUpdate(modification); - } - } - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = RealmFactory.GetForWrite()) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 0a065c9dbc..f73d92f5c2 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -16,7 +17,7 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Input; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -26,7 +27,7 @@ namespace osu.Game.Overlays.KeyBinding public class KeyBindingRow : Container, IFilterable { private readonly object action; - private readonly IEnumerable bindings; + private readonly IEnumerable bindings; private const float transition_time = 150; @@ -52,9 +53,9 @@ namespace osu.Game.Overlays.KeyBinding private FillFlowContainer cancelAndClearButtons; private FillFlowContainer buttons; - public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); + public IEnumerable FilterTerms => bindings.Select(b => ((IKeyBinding)b).KeyCombination.ReadableString()).Prepend((string)text.Text); - public KeyBindingRow(object action, IEnumerable bindings) + public KeyBindingRow(object action, List bindings) { this.action = action; this.bindings = bindings; @@ -67,7 +68,7 @@ namespace osu.Game.Overlays.KeyBinding } [Resolved] - private RealmKeyBindingStore store { get; set; } + private RealmContextFactory realmFactory { get; set; } [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -127,7 +128,12 @@ namespace osu.Game.Overlays.KeyBinding { var button = buttons[i++]; button.UpdateKeyCombination(d); - store.Update((IHasGuidPrimaryKey)button.KeyBinding, k => k.KeyCombination = button.KeyBinding.KeyCombination); + + using (var write = realmFactory.GetForWrite()) + { + var binding = write.Context.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + binding.KeyCombination = button.KeyBinding.KeyCombination; + } } } @@ -286,7 +292,11 @@ namespace osu.Game.Overlays.KeyBinding { if (bindTarget != null) { - store.Update((IHasGuidPrimaryKey)bindTarget.KeyBinding, k => k.KeyCombination = bindTarget.KeyBinding.KeyCombination); + using (var write = realmFactory.GetForWrite()) + { + var binding = write.Context.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); + binding.KeyCombination = bindTarget.KeyBinding.KeyCombination; + } bindTarget.IsBinding = false; Schedule(() => @@ -360,7 +370,7 @@ namespace osu.Game.Overlays.KeyBinding public class KeyButton : Container { - public readonly IKeyBinding KeyBinding; + public readonly RealmKeyBinding KeyBinding; private readonly Box box; public readonly OsuSpriteText Text; @@ -382,8 +392,11 @@ namespace osu.Game.Overlays.KeyBinding } } - public KeyButton(IKeyBinding keyBinding) + public KeyButton(RealmKeyBinding keyBinding) { + if (keyBinding.IsManaged) + throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(keyBinding)); + KeyBinding = keyBinding; Margin = new MarginPadding(padding); @@ -416,7 +429,7 @@ namespace osu.Game.Overlays.KeyBinding Margin = new MarginPadding(5), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = keyBinding.KeyCombination.ReadableString(), + Text = ((IKeyBinding)keyBinding).KeyCombination.ReadableString(), }, }; } @@ -455,8 +468,10 @@ namespace osu.Game.Overlays.KeyBinding public void UpdateKeyCombination(KeyCombination newCombination) { - KeyBinding.KeyCombination = newCombination; - Text.Text = KeyBinding.KeyCombination.ReadableString(); + var keyBinding = (IKeyBinding)KeyBinding; + + keyBinding.KeyCombination = newCombination; + Text.Text = keyBinding.KeyCombination.ReadableString(); } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index 0f95d07da8..a23f22cf57 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.KeyBinding int intKey = (int)defaultGroup.Key; // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey))) + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey)).ToList()) { AllowMainMouseButtons = Ruleset != null, Defaults = defaultGroup.Select(d => d.KeyCombination) From fcb4a53f37250cd4a7f9ac7b900747d4b7481f4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:07:35 +0900 Subject: [PATCH 054/107] Rename realm persisted properties to avoid casting necessity --- .../Database/TestRealmKeyBindingStore.cs | 16 +++++++-------- osu.Game/Input/Bindings/RealmKeyBinding.cs | 20 ++++++++++--------- osu.Game/Input/RealmKeyBindingStore.cs | 10 +++++----- osu.Game/OsuGameBase.cs | 4 ++-- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 14 ++++++------- .../KeyBinding/KeyBindingsSubsection.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 6 +++--- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 426593f5de..1a7e0d67c5 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -45,8 +45,8 @@ namespace osu.Game.Tests.Database Assert.That(query().Count, Is.EqualTo(3)); - Assert.That(query().Where(k => k.Action == (int)GlobalAction.Back).Count, Is.EqualTo(1)); - Assert.That(query().Where(k => k.Action == (int)GlobalAction.Select).Count, Is.EqualTo(2)); + Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1)); + Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2)); } private IQueryable query() => realmContextFactory.Context.All(); @@ -58,22 +58,22 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer); - var backBinding = query().Single(k => k.Action == (int)GlobalAction.Back); + var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back); - Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); + Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); var binding = backBinding; realmContextFactory.Context.Write(() => { - ((IKeyBinding)binding).KeyCombination = new KeyCombination(InputKey.BackSpace); + binding.KeyCombination = new KeyCombination(InputKey.BackSpace); }); - Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); + Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); // check still correct after re-query. - backBinding = query().Single(k => k.Action == (int)GlobalAction.Back); - Assert.That(((IKeyBinding)backBinding).KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); + backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back); + Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); } [TearDown] diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 1e690ddbab..ecffc1fd62 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -7,7 +7,7 @@ using Realms; namespace osu.Game.Input.Bindings { - [MapTo("KeyBinding")] + [MapTo(nameof(KeyBinding))] public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] @@ -17,20 +17,22 @@ namespace osu.Game.Input.Bindings public int? Variant { get; set; } - KeyCombination IKeyBinding.KeyCombination + public KeyCombination KeyCombination { - get => KeyCombination; - set => KeyCombination = value.ToString(); + get => KeyCombinationString; + set => KeyCombinationString = value.ToString(); } - object IKeyBinding.Action + public object Action { - get => Action; - set => Action = (int)value; + get => ActionInt; + set => ActionInt = (int)value; } - public int Action { get; set; } + [MapTo(nameof(Action))] + public int ActionInt { get; set; } - public string KeyCombination { get; set; } + [MapTo(nameof(KeyCombination))] + public string KeyCombinationString { get; set; } } } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 478f4792f8..8e43811c36 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -28,9 +28,9 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in RealmFactory.Context.All().Where(b => (GlobalAction)b.Action == globalAction)) + foreach (var action in RealmFactory.Context.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) { - string str = ((IKeyBinding)action).KeyCombination.ReadableString(); + string str = action.KeyCombination.ReadableString(); // even if found, the readable string may be empty for an unbound action. if (str.Length > 0) @@ -66,7 +66,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = usage.Context.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.Action == (int)group.Key); + int count = usage.Context.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -78,8 +78,8 @@ namespace osu.Game.Input usage.Context.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), - KeyCombination = insertable.KeyCombination.ToString(), - Action = (int)insertable.Action, + KeyCombinationString = insertable.KeyCombination.ToString(), + ActionInt = (int)insertable.Action, RulesetID = rulesetId, Variant = variant }); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 01f161cfd9..192867b8c8 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -342,8 +342,8 @@ namespace osu.Game realm.Context.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), - KeyCombination = dkb.KeyCombination.ToString(), - Action = (int)dkb.Action, + KeyCombinationString = dkb.KeyCombination.ToString(), + ActionInt = (int)dkb.Action, RulesetID = dkb.RulesetID, Variant = dkb.Variant }); diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index f73d92f5c2..34cdfd18fa 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.KeyBinding private FillFlowContainer cancelAndClearButtons; private FillFlowContainer buttons; - public IEnumerable FilterTerms => bindings.Select(b => ((IKeyBinding)b).KeyCombination.ReadableString()).Prepend((string)text.Text); + public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); public KeyBindingRow(object action, List bindings) { @@ -132,7 +132,7 @@ namespace osu.Game.Overlays.KeyBinding using (var write = realmFactory.GetForWrite()) { var binding = write.Context.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); - binding.KeyCombination = button.KeyBinding.KeyCombination; + binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; } } } @@ -295,7 +295,7 @@ namespace osu.Game.Overlays.KeyBinding using (var write = realmFactory.GetForWrite()) { var binding = write.Context.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); - binding.KeyCombination = bindTarget.KeyBinding.KeyCombination; + binding.KeyCombinationString = bindTarget.KeyBinding.KeyCombinationString; } bindTarget.IsBinding = false; @@ -429,7 +429,7 @@ namespace osu.Game.Overlays.KeyBinding Margin = new MarginPadding(5), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = ((IKeyBinding)keyBinding).KeyCombination.ReadableString(), + Text = keyBinding.KeyCombination.ReadableString(), }, }; } @@ -468,10 +468,8 @@ namespace osu.Game.Overlays.KeyBinding public void UpdateKeyCombination(KeyCombination newCombination) { - var keyBinding = (IKeyBinding)KeyBinding; - - keyBinding.KeyCombination = newCombination; - Text.Text = keyBinding.KeyCombination.ReadableString(); + KeyBinding.KeyCombination = newCombination; + Text.Text = KeyBinding.KeyCombination.ReadableString(); } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index a23f22cf57..fae42f5492 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.KeyBinding int intKey = (int)defaultGroup.Key; // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals(intKey)).ToList()) + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) { AllowMainMouseButtons = Ruleset != null, Defaults = defaultGroup.Select(d => d.KeyCombination) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 7bb0eb894c..305a17126a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -165,13 +165,13 @@ namespace osu.Game.Overlays.Toolbar if (Hotkey != null) { - realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.Action == (int)Hotkey.Value); + realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); if (realmKeyBinding != null) { realmKeyBinding.PropertyChanged += (sender, args) => { - if (args.PropertyName == nameof(realmKeyBinding.KeyCombination)) + if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) updateKeyBindingTooltip(); }; } @@ -223,7 +223,7 @@ namespace osu.Game.Overlays.Toolbar { if (realmKeyBinding != null) { - KeyCombination? binding = ((IKeyBinding)realmKeyBinding).KeyCombination; + KeyCombination? binding = realmKeyBinding.KeyCombination; var keyBindingString = binding?.ReadableString(); From 5fa3a22f28ac40a857b1a21b49c13766ee8e752d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:13:30 +0900 Subject: [PATCH 055/107] Remove unused RealmBackedStore base class --- osu.Game/Database/RealmBackedStore.cs | 29 -------------------------- osu.Game/Input/RealmKeyBindingStore.cs | 15 ++++++------- 2 files changed, 8 insertions(+), 36 deletions(-) delete mode 100644 osu.Game/Database/RealmBackedStore.cs diff --git a/osu.Game/Database/RealmBackedStore.cs b/osu.Game/Database/RealmBackedStore.cs deleted file mode 100644 index bc67e332fe..0000000000 --- a/osu.Game/Database/RealmBackedStore.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Platform; - -#nullable enable - -namespace osu.Game.Database -{ - public abstract class RealmBackedStore - { - protected readonly Storage? Storage; - - protected readonly IRealmFactory RealmFactory; - - protected RealmBackedStore(IRealmFactory realmFactory, Storage? storage = null) - { - RealmFactory = realmFactory; - Storage = storage; - } - - /// - /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. - /// - public virtual void Cleanup() - { - } - } -} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 8e43811c36..756df6434e 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Input.Bindings; -using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Rulesets; @@ -14,11 +13,13 @@ using osu.Game.Rulesets; namespace osu.Game.Input { - public class RealmKeyBindingStore : RealmBackedStore + public class RealmKeyBindingStore { - public RealmKeyBindingStore(RealmContextFactory realmFactory, Storage? storage = null) - : base(realmFactory, storage) + private readonly RealmContextFactory realmFactory; + + public RealmKeyBindingStore(RealmContextFactory realmFactory) { + this.realmFactory = realmFactory; } /// @@ -28,7 +29,7 @@ namespace osu.Game.Input /// A set of display strings for all the user's key configuration for the action. public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in RealmFactory.Context.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) + foreach (var action in realmFactory.Context.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) { string str = action.KeyCombination.ReadableString(); @@ -52,7 +53,7 @@ namespace osu.Game.Input { var instance = ruleset.CreateInstance(); - using (RealmFactory.GetForWrite()) + using (realmFactory.GetForWrite()) { foreach (var variant in instance.AvailableVariants) insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); @@ -61,7 +62,7 @@ namespace osu.Game.Input private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { - using (var usage = RealmFactory.GetForWrite()) + using (var usage = realmFactory.GetForWrite()) { // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) From 8442b34e84032f9365a6575cece19c86690999dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:24:19 +0900 Subject: [PATCH 056/107] Tidy up write usage class --- osu.Game/Database/IRealmFactory.cs | 2 +- osu.Game/Database/RealmContextFactory.cs | 81 ++++++------------------ osu.Game/Database/RealmWriteUsage.cs | 55 ---------------- 3 files changed, 22 insertions(+), 116 deletions(-) delete mode 100644 osu.Game/Database/RealmWriteUsage.cs diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 0fffc3d7be..4a92a5683b 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -22,6 +22,6 @@ namespace osu.Game.Database /// This method may block if a write is already active on a different thread. /// /// A usage containing a usable context. - RealmWriteUsage GetForWrite(); + RealmContextFactory.RealmWriteUsage GetForWrite(); } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index ea3549a9cd..dc8761fb3c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; @@ -13,6 +14,7 @@ namespace osu.Game.Database public class RealmContextFactory : Component, IRealmFactory { private readonly Storage storage; + private const string database_name = @"client"; private const int schema_version = 5; @@ -22,20 +24,11 @@ namespace osu.Game.Database /// private readonly object writeLock = new object(); - private ThreadLocal refreshCompleted = new ThreadLocal(); - - private bool rollbackRequired; - private int currentWriteUsages; - private Transaction currentWriteTransaction; - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); - private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Refreshes"); - private static readonly GlobalStatistic commits = GlobalStatistics.Get("Realm", "Commits"); - private static readonly GlobalStatistic rollbacks = GlobalStatistics.Get("Realm", "Rollbacks"); - private static readonly GlobalStatistic contexts_open = GlobalStatistics.Get("Realm", "Contexts (Open)"); + private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); private Realm context; @@ -72,24 +65,8 @@ namespace osu.Game.Database writes.Value++; Monitor.Enter(writeLock); - Realm realm; - - try - { - realm = createContext(); - - currentWriteTransaction ??= realm.BeginWrite(); - } - catch - { - // retrieval of a context could trigger a fatal error. - Monitor.Exit(writeLock); - throw; - } - Interlocked.Increment(ref currentWriteUsages); - - return new RealmWriteUsage(realm, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; + return new RealmWriteUsage(this); } protected override void Update() @@ -111,41 +88,25 @@ namespace osu.Game.Database }); } - private void usageCompleted(RealmWriteUsage usage) - { - int usages = Interlocked.Decrement(ref currentWriteUsages); - - try - { - rollbackRequired |= usage.RollbackRequired; - - if (usages == 0) - { - if (rollbackRequired) - { - rollbacks.Value++; - currentWriteTransaction?.Rollback(); - } - else - { - commits.Value++; - currentWriteTransaction?.Commit(); - } - - currentWriteTransaction = null; - rollbackRequired = false; - - refreshCompleted = new ThreadLocal(); - } - } - finally - { - Monitor.Exit(writeLock); - } - } - private void onMigration(Migration migration, ulong lastSchemaVersion) { } + + public class RealmWriteUsage : InvokeOnDisposal + { + public readonly Realm Context; + + public RealmWriteUsage(RealmContextFactory factory) + : base(factory, usageCompleted) + { + Context = factory.createContext(); + Context.BeginWrite(); + } + + private static void usageCompleted(RealmContextFactory factory) + { + Monitor.Exit(factory.writeLock); + } + } } } diff --git a/osu.Game/Database/RealmWriteUsage.cs b/osu.Game/Database/RealmWriteUsage.cs deleted file mode 100644 index 35e30e8123..0000000000 --- a/osu.Game/Database/RealmWriteUsage.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using Realms; - -namespace osu.Game.Database -{ - public class RealmWriteUsage : IDisposable - { - public readonly Realm Context; - private readonly Action usageCompleted; - - public bool RollbackRequired { get; private set; } - - public RealmWriteUsage(Realm context, Action onCompleted) - { - Context = context; - usageCompleted = onCompleted; - } - - /// - /// Whether this write usage will commit a transaction on completion. - /// If false, there is a parent usage responsible for transaction commit. - /// - public bool IsTransactionLeader; - - private bool isDisposed; - - protected void Dispose(bool disposing) - { - if (isDisposed) return; - - isDisposed = true; - - usageCompleted?.Invoke(this); - } - - public void Rollback(Exception error = null) - { - RollbackRequired = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - ~RealmWriteUsage() - { - Dispose(false); - } - } -} From 9bf9a8c351ac533bbdd4b33a7f1659dd9544168f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:36:05 +0900 Subject: [PATCH 057/107] Remove Live<> wrapper until it is needed --- osu.Game/Database/Live.cs | 104 --------------------------- osu.Game/Database/RealmExtensions.cs | 27 ------- 2 files changed, 131 deletions(-) delete mode 100644 osu.Game/Database/Live.cs diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs deleted file mode 100644 index 49218ddd6e..0000000000 --- a/osu.Game/Database/Live.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Threading; -using Realms; - -#nullable enable - -namespace osu.Game.Database -{ - /// - /// Provides a method of passing realm live objects across threads in a safe fashion. - /// - /// - /// To consume this as a live instance, the live object should be stored and accessed via each time. - /// To consume this as a detached instance, assign to a variable of type . The implicit conversion will handle detaching an instance. - /// - /// The underlying object type. Should be a with a primary key provided via . - public class Live : IEquatable>, IHasGuidPrimaryKey - where T : RealmObject, IHasGuidPrimaryKey - { - /// - /// The primary key of the object. - /// - public Guid Guid { get; } - - public string ID - { - get => Guid.ToString(); - set => throw new NotImplementedException(); - } - - private readonly ThreadLocal threadValues; - - private readonly T original; - - private readonly IRealmFactory contextFactory; - - public Live(T item, IRealmFactory contextFactory) - { - this.contextFactory = contextFactory; - - original = item; - Guid = item.Guid; - - threadValues = new ThreadLocal(getThreadLocalValue); - - // the instance passed in may not be in a managed state. - // for now let's immediately retrieve a managed object on the current thread. - // in the future we may want to delay this until the first access (only populating the Guid at construction time). - if (!item.IsManaged) - original = Get(); - } - - private T getThreadLocalValue() - { - var context = contextFactory.Context; - - // only use the original if no context is available or the source realm is the same. - if (context == null || original.Realm?.IsSameInstance(context) == true) return original; - - return context.Find(ID); - } - - /// - /// Retrieve a live reference to the data. - /// - public T Get() => threadValues.Value; - - /// - /// Retrieve a detached copy of the data. - /// - public T Detach() => Get().Detach(); - - /// - /// Wrap a property of this instance as its own live access object. - /// - /// The child to return. - /// The underlying child object type. Should be a with a primary key provided via . - /// A wrapped instance of the child. - public Live WrapChild(Func lookup) - where TChild : RealmObject, IHasGuidPrimaryKey => new Live(lookup(Get()), contextFactory); - - /// - /// Perform a write operation on this live object. - /// - /// The action to perform. - public void PerformUpdate(Action perform) - { - using (contextFactory.GetForWrite()) - perform(Get()); - } - - public static implicit operator T?(Live? wrapper) - => wrapper?.Detach() ?? null; - - public static implicit operator Live(T obj) => obj.WrapAsUnmanaged(); - - public bool Equals(Live? other) => other != null && other.Guid == Guid; - - public override string ToString() => Get().ToString(); - } -} diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index b25299bf23..07d397ca6c 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using AutoMapper; using osu.Game.Beatmaps; @@ -69,31 +68,5 @@ namespace osu.Game.Database return mapper.Map(item); } - - /// - /// Wrap a managed instance of a realm object in a . - /// - /// The item to wrap. - /// A factory to retrieve realm contexts from. - /// The type of object. - /// A wrapped instance of the provided item. - public static Live Wrap(this T item, IRealmFactory contextFactory) - where T : RealmObject, IHasGuidPrimaryKey => new Live(item, contextFactory); - - /// - /// Wrap an unmanaged instance of a realm object in a . - /// - /// The item to wrap. - /// The type of object. - /// A wrapped instance of the provided item. - /// Throws if the provided item is managed. - public static Live WrapAsUnmanaged(this T item) - where T : RealmObject, IHasGuidPrimaryKey - { - if (item.IsManaged) - throw new ArgumentException("Provided item must not be managed", nameof(item)); - - return new Live(item, null); - } } } From 674e78fd93858e00ff4f54b930c70d5172ca528f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Jan 2021 18:38:30 +0900 Subject: [PATCH 058/107] Fix broken xmldoc --- osu.Game/Database/RealmExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 07d397ca6c..04b0820dd9 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -56,7 +56,7 @@ namespace osu.Game.Database } /// - /// Create a detached copy of the each item in the list. + /// Create a detached copy of the item. /// /// The managed to detach. /// The type of object. From 4759797d152c4eeabf128671f5a49f7fd0725c6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 15:51:07 +0900 Subject: [PATCH 059/107] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 492c88c7e4..8a74f8ddce 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1a762be9c9..e43da1df7b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -27,7 +27,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 93be3645ee..aecc8cd5eb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From af1509d892b0788cd6458865d1c004919d560091 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 15:51:19 +0900 Subject: [PATCH 060/107] Remove unused variable (but add back pending writes counter) --- osu.Game/Database/RealmContextFactory.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index dc8761fb3c..c18cd31bfa 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -24,12 +24,11 @@ namespace osu.Game.Database /// private readonly object writeLock = new object(); - private int currentWriteUsages; - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); + private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); private Realm context; @@ -63,9 +62,10 @@ namespace osu.Game.Database public RealmWriteUsage GetForWrite() { writes.Value++; + pending_writes.Value++; + Monitor.Enter(writeLock); - Interlocked.Increment(ref currentWriteUsages); return new RealmWriteUsage(this); } @@ -106,6 +106,7 @@ namespace osu.Game.Database private static void usageCompleted(RealmContextFactory factory) { Monitor.Exit(factory.writeLock); + pending_writes.Value--; } } } From 8a08d3f4efe807ca77f0d4fe893962a3d6ec9bfc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:13:10 +0900 Subject: [PATCH 061/107] Fix transactions not actually being committed --- osu.Game/Database/RealmContextFactory.cs | 39 +++++++++++++++---- osu.Game/Input/RealmKeyBindingStore.cs | 13 +++---- osu.Game/OsuGameBase.cs | 8 ++-- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 10 +++-- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c18cd31bfa..0631acc750 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Threading; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; @@ -92,19 +92,42 @@ namespace osu.Game.Database { } - public class RealmWriteUsage : InvokeOnDisposal + /// + /// A transaction used for making changes to realm data. + /// + public class RealmWriteUsage : IDisposable { - public readonly Realm Context; + public readonly Realm Realm; - public RealmWriteUsage(RealmContextFactory factory) - : base(factory, usageCompleted) + private readonly RealmContextFactory factory; + private readonly Transaction transaction; + + internal RealmWriteUsage(RealmContextFactory factory) { - Context = factory.createContext(); - Context.BeginWrite(); + this.factory = factory; + + Realm = factory.createContext(); + transaction = Realm.BeginWrite(); } - private static void usageCompleted(RealmContextFactory factory) + /// + /// Commit all changes made in this transaction. + /// + public void Commit() => transaction.Commit(); + + /// + /// Revert all changes made in this transaction. + /// + public void Rollback() => transaction.Rollback(); + + /// + /// Disposes this instance, calling the initially captured action. + /// + public virtual void Dispose() { + // rollback if not explicitly committed. + transaction?.Dispose(); + Monitor.Exit(factory.writeLock); pending_writes.Value--; } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 756df6434e..d55d2362fe 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -53,11 +53,8 @@ namespace osu.Game.Input { var instance = ruleset.CreateInstance(); - using (realmFactory.GetForWrite()) - { - foreach (var variant in instance.AvailableVariants) - insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); - } + foreach (var variant in instance.AvailableVariants) + insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); } private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) @@ -67,7 +64,7 @@ namespace osu.Game.Input // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) { - int count = usage.Context.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)group.Key); + int count = usage.Realm.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)group.Key); int aimCount = group.Count(); if (aimCount <= count) @@ -76,7 +73,7 @@ namespace osu.Game.Input foreach (var insertable in group.Skip(count).Take(aimCount - count)) { // insert any defaults which are missing. - usage.Context.Add(new RealmKeyBinding + usage.Realm.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), KeyCombinationString = insertable.KeyCombination.ToString(), @@ -86,6 +83,8 @@ namespace osu.Game.Input }); } } + + usage.Commit(); } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 192867b8c8..a755fdb379 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -330,16 +330,16 @@ namespace osu.Game private void migrateDataToRealm() { using (var db = contextFactory.GetForWrite()) - using (var realm = realmFactory.GetForWrite()) + using (var usage = realmFactory.GetForWrite()) { var existingBindings = db.Context.DatabasedKeyBinding; // only migrate data if the realm database is empty. - if (!realm.Context.All().Any()) + if (!usage.Realm.All().Any()) { foreach (var dkb in existingBindings) { - realm.Context.Add(new RealmKeyBinding + usage.Realm.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), KeyCombinationString = dkb.KeyCombination.ToString(), @@ -351,6 +351,8 @@ namespace osu.Game } db.Context.RemoveRange(existingBindings); + + usage.Commit(); } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 34cdfd18fa..bfabc8008d 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -129,10 +129,12 @@ namespace osu.Game.Overlays.KeyBinding var button = buttons[i++]; button.UpdateKeyCombination(d); - using (var write = realmFactory.GetForWrite()) + using (var usage = realmFactory.GetForWrite()) { - var binding = write.Context.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; + + usage.Commit(); } } } @@ -294,8 +296,10 @@ namespace osu.Game.Overlays.KeyBinding { using (var write = realmFactory.GetForWrite()) { - var binding = write.Context.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); + var binding = write.Realm.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); binding.KeyCombinationString = bindTarget.KeyBinding.KeyCombinationString; + + write.Commit(); } bindTarget.IsBinding = false; From e3c5a909e4a641c4d79aa0da889e1f921b063b31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:30:57 +0900 Subject: [PATCH 062/107] Fix known non-null variable --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 305a17126a..0f270cbece 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -223,9 +223,7 @@ namespace osu.Game.Overlays.Toolbar { if (realmKeyBinding != null) { - KeyCombination? binding = realmKeyBinding.KeyCombination; - - var keyBindingString = binding?.ReadableString(); + var keyBindingString = realmKeyBinding.KeyCombination.ReadableString(); if (!string.IsNullOrEmpty(keyBindingString)) keyBindingTooltip.Text = $" ({keyBindingString})"; From df08d964a5c3894224f59c1d0562e107767cb062 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:31:18 +0900 Subject: [PATCH 063/107] Mark the types which have been migrated in OsuDbContext --- osu.Game/Database/OsuDbContext.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 2ae07b3cf8..8ca65525db 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -24,13 +24,15 @@ namespace osu.Game.Database public DbSet BeatmapDifficulty { get; set; } public DbSet BeatmapMetadata { get; set; } public DbSet BeatmapSetInfo { get; set; } - public DbSet DatabasedKeyBinding { get; set; } public DbSet DatabasedSetting { get; set; } public DbSet FileInfo { get; set; } public DbSet RulesetInfo { get; set; } public DbSet SkinInfo { get; set; } public DbSet ScoreInfo { get; set; } + // migrated to realm + public DbSet DatabasedKeyBinding { get; set; } + private readonly string connectionString; private static readonly Lazy logger = new Lazy(() => new OsuDbLoggerFactory()); From 8d071f97fb1e193b64c379c82d35374b316f7226 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:33:03 +0900 Subject: [PATCH 064/107] Early return --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 0f270cbece..17bc913b43 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -163,21 +163,20 @@ namespace osu.Game.Overlays.Toolbar { base.LoadComplete(); - if (Hotkey != null) + if (Hotkey == null) return; + + realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); + + if (realmKeyBinding != null) { - realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value); - - if (realmKeyBinding != null) + realmKeyBinding.PropertyChanged += (sender, args) => { - realmKeyBinding.PropertyChanged += (sender, args) => - { - if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) - updateKeyBindingTooltip(); - }; - } - - updateKeyBindingTooltip(); + if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString)) + updateKeyBindingTooltip(); + }; } + + updateKeyBindingTooltip(); } protected override bool OnMouseDown(MouseDownEvent e) => true; From fd582f521c567c4554b8b60a521189480a882920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:33:55 +0900 Subject: [PATCH 065/107] Reduce lifetime of realm context usage in detach scenario --- .../KeyBinding/KeyBindingsSubsection.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index fae42f5492..1cd600a72d 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -37,21 +37,21 @@ namespace osu.Game.Overlays.KeyBinding { var rulesetId = Ruleset?.ID; + List bindings; + using (var realm = realmFactory.GetForRead()) + bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + + foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { - var bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + int intKey = (int)defaultGroup.Key; - foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) + // one row per valid action. + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) { - int intKey = (int)defaultGroup.Key; - - // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) - { - AllowMainMouseButtons = Ruleset != null, - Defaults = defaultGroup.Select(d => d.KeyCombination) - }); - } + AllowMainMouseButtons = Ruleset != null, + Defaults = defaultGroup.Select(d => d.KeyCombination) + }); } Add(new ResetButton From f26c6210f36c9e73d18cb971ff950823f060900b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 16:36:24 +0900 Subject: [PATCH 066/107] Remove unnecessary Take() call and refactor default group logic naming --- osu.Game/Input/RealmKeyBindingStore.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index d55d2362fe..f5c4b646c1 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -62,22 +62,21 @@ namespace osu.Game.Input using (var usage = realmFactory.GetForWrite()) { // compare counts in database vs defaults - foreach (var group in defaults.GroupBy(k => k.Action)) + foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) { - int count = usage.Realm.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)group.Key); - int aimCount = group.Count(); + int existingCount = usage.Realm.All().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key); - if (aimCount <= count) + if (defaultsForAction.Count() <= existingCount) continue; - foreach (var insertable in group.Skip(count).Take(aimCount - count)) + foreach (var k in defaultsForAction.Skip(existingCount)) { // insert any defaults which are missing. usage.Realm.Add(new RealmKeyBinding { ID = Guid.NewGuid().ToString(), - KeyCombinationString = insertable.KeyCombination.ToString(), - ActionInt = (int)insertable.Action, + KeyCombinationString = k.KeyCombination.ToString(), + ActionInt = (int)k.Action, RulesetID = rulesetId, Variant = variant }); From 693602513ea5d7b5fbefd8dfb5f2d43b37dbed2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 14:22:48 +0900 Subject: [PATCH 067/107] Update test to use GetForWrite --- osu.Game.Tests/Database/TestRealmKeyBindingStore.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 1a7e0d67c5..6c0811f633 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -11,6 +11,7 @@ using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; +using Realms; namespace osu.Game.Tests.Database { @@ -62,12 +63,15 @@ namespace osu.Game.Tests.Database Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); - var binding = backBinding; + var tsr = ThreadSafeReference.Create(backBinding); - realmContextFactory.Context.Write(() => + using (var usage = realmContextFactory.GetForWrite()) { + var binding = usage.Realm.ResolveReference(tsr); binding.KeyCombination = new KeyCombination(InputKey.BackSpace); - }); + + usage.Commit(); + } Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); From 3e366b1f1545ca0f44e7b28bac1a82985e36b5f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 14:26:06 +0900 Subject: [PATCH 068/107] Ensure the main realm context is closed when the factory is disposed --- osu.Game.Tests/Database/TestRealmKeyBindingStore.cs | 1 + osu.Game/Database/RealmContextFactory.cs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 6c0811f633..cac331451b 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -83,6 +83,7 @@ namespace osu.Game.Tests.Database [TearDown] public void TearDown() { + realmContextFactory.Dispose(); storage.DeleteDirectory(string.Empty); } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0631acc750..b76006cd88 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -36,6 +36,9 @@ namespace osu.Game.Database { get { + if (IsDisposed) + throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory"); + if (context == null) { context = createContext(); @@ -92,6 +95,14 @@ namespace osu.Game.Database { } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + context?.Dispose(); + context = null; + } + /// /// A transaction used for making changes to realm data. /// From ddc63662ba9da2635bd4d3b851b1ee8922eea453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 16:39:04 +0100 Subject: [PATCH 069/107] Dispose realm in RealmWriteUsage cleanup --- osu.Game/Database/RealmContextFactory.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index b76006cd88..f735098e88 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -138,6 +138,7 @@ namespace osu.Game.Database { // rollback if not explicitly committed. transaction?.Dispose(); + Realm?.Dispose(); Monitor.Exit(factory.writeLock); pending_writes.Value--; From 0f8f0434f95bce74cf9c0e3ec38e2ad4476b3f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 17:03:58 +0100 Subject: [PATCH 070/107] Remove EF store again after mis-merge Was originally deleted in 536e7229d0cb82504a39f0a18e120da91e0b0f12. --- osu.Game/Input/KeyBindingStore.cs | 108 ------------------------------ 1 file changed, 108 deletions(-) delete mode 100644 osu.Game/Input/KeyBindingStore.cs diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs deleted file mode 100644 index b25b00eb84..0000000000 --- a/osu.Game/Input/KeyBindingStore.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Input.Bindings; -using osu.Framework.Platform; -using osu.Game.Database; -using osu.Game.Input.Bindings; -using osu.Game.Rulesets; - -namespace osu.Game.Input -{ - public class KeyBindingStore : DatabaseBackedStore - { - public event Action KeyBindingChanged; - - public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) - : base(contextFactory, storage) - { - using (ContextFactory.GetForWrite()) - { - foreach (var info in rulesets.AvailableRulesets) - { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); - } - } - } - - public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); - - /// - /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. - /// - /// The action to lookup. - /// A set of display strings for all the user's key configuration for the action. - public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) - { - foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction)) - { - string str = action.KeyCombination.ReadableString(); - - // even if found, the readable string may be empty for an unbound action. - if (str.Length > 0) - yield return str; - } - } - - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) - { - using (var usage = ContextFactory.GetForWrite()) - { - // compare counts in database vs defaults - foreach (var group in defaults.GroupBy(k => k.Action)) - { - int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); - int aimCount = group.Count(); - - if (aimCount <= count) - continue; - - foreach (var insertable in group.Skip(count).Take(aimCount - count)) - { - // insert any defaults which are missing. - usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding - { - KeyCombination = insertable.KeyCombination, - Action = insertable.Action, - RulesetID = rulesetId, - Variant = variant - }); - - // required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686) - usage.Context.SaveChanges(); - } - } - } - } - - /// - /// Retrieve s for a specified ruleset/variant content. - /// - /// The ruleset's internal ID. - /// An optional variant. - /// - public List Query(int? rulesetId = null, int? variant = null) => - ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); - - public void Update(KeyBinding keyBinding) - { - using (ContextFactory.GetForWrite()) - { - var dbKeyBinding = (DatabasedKeyBinding)keyBinding; - Refresh(ref dbKeyBinding); - - if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) - return; - - dbKeyBinding.KeyCombination = keyBinding.KeyCombination; - } - - KeyBindingChanged?.Invoke(); - } - } -} From cd8401c39c591955da4d2be52d4182dea6b6aa3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 17:20:33 +0100 Subject: [PATCH 071/107] Suppress nuget warning due to including beta realm --- Directory.Build.props | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2e1873a9ed..8e0693f8d5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,12 +33,16 @@ DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 + NU5104: + This is triggered on osu.Game due to using a beta/prerelease version of realm. + Warning suppression can be removed after migrating off of a beta release. + CA9998: Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. The entire package will be able to be removed after migrating to .NET 5, as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;CA9998 + $(NoWarn);NU1701;NU5104;CA9998 false From 15db0e97d7f84729034652e2c63e988b51ac15cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Jan 2021 18:06:32 +0900 Subject: [PATCH 072/107] Update realm version --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e48c103700..7c49a250c4 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + From 68f2e7f61ae35e800fd34e1dff4e1bdf6a411164 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Jan 2021 18:22:23 +0900 Subject: [PATCH 073/107] Use realm support for Guid --- osu.Game/Database/IHasGuidPrimaryKey.cs | 12 +----------- osu.Game/Database/RealmContextFactory.cs | 11 ++++++++++- osu.Game/Input/Bindings/RealmKeyBinding.cs | 3 ++- osu.Game/Input/RealmKeyBindingStore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index 3b0888c654..ca41d70210 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; using Realms; @@ -11,16 +10,7 @@ namespace osu.Game.Database public interface IHasGuidPrimaryKey { [JsonIgnore] - [Ignored] - public Guid Guid - { - get => new Guid(ID); - set => ID = value.ToString(); - } - - [JsonIgnore] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] [PrimaryKey] - string ID { get; set; } + public Guid ID { get; set; } } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index f735098e88..918ec1eb53 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -7,7 +7,9 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Game.Input.Bindings; using Realms; +using Realms.Schema; namespace osu.Game.Database { @@ -17,7 +19,7 @@ namespace osu.Game.Database private const string database_name = @"client"; - private const int schema_version = 5; + private const int schema_version = 6; /// /// Lock object which is held for the duration of a write operation (via ). @@ -93,6 +95,13 @@ namespace osu.Game.Database private void onMigration(Migration migration, ulong lastSchemaVersion) { + switch (lastSchemaVersion) + { + case 5: + // let's keep things simple. changing the type of the primary key is a bit involved. + migration.NewRealm.RemoveAll(); + break; + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index ecffc1fd62..d10cb6af83 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Input.Bindings; using osu.Game.Database; using Realms; @@ -11,7 +12,7 @@ namespace osu.Game.Input.Bindings public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] - public string ID { get; set; } + public Guid ID { get; set; } public int? RulesetID { get; set; } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index f5c4b646c1..76e65c1c70 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -74,7 +74,7 @@ namespace osu.Game.Input // insert any defaults which are missing. usage.Realm.Add(new RealmKeyBinding { - ID = Guid.NewGuid().ToString(), + ID = Guid.NewGuid(), KeyCombinationString = k.KeyCombination.ToString(), ActionInt = (int)k.Action, RulesetID = rulesetId, diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ae1ec97a4c..d98a20925a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -341,7 +341,7 @@ namespace osu.Game { usage.Realm.Add(new RealmKeyBinding { - ID = Guid.NewGuid().ToString(), + ID = Guid.NewGuid(), KeyCombinationString = dkb.KeyCombination.ToString(), ActionInt = (int)dkb.Action, RulesetID = dkb.RulesetID, From f6c20095094bdbf26c6ffe55dfdab12fe2f7e083 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Jan 2021 20:10:10 +0900 Subject: [PATCH 074/107] Remove unused using --- osu.Game/Database/RealmContextFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 918ec1eb53..d7e35f736e 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -9,7 +9,6 @@ using osu.Framework.Platform; using osu.Framework.Statistics; using osu.Game.Input.Bindings; using Realms; -using Realms.Schema; namespace osu.Game.Database { From d2bf3a58057f1ea5917e16bdc2288e7e068282ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 19:01:58 +0900 Subject: [PATCH 075/107] Add ignore files to avoid copying realm management/pipes --- osu.Game/IO/OsuStorage.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8097f61ea4..f7abd2a6f1 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -33,12 +33,17 @@ namespace osu.Game.IO private readonly StorageConfigManager storageConfig; private readonly Storage defaultStorage; - public override string[] IgnoreDirectories => new[] { "cache" }; + public override string[] IgnoreDirectories => new[] + { + "cache", + "client.realm.management" + }; public override string[] IgnoreFiles => new[] { "framework.ini", - "storage.ini" + "storage.ini", + "client.realm.note" }; public OsuStorage(GameHost host, Storage defaultStorage) From 34a7ce912eaf46cdf691d1525a8f4dfa105d4476 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 19:02:09 +0900 Subject: [PATCH 076/107] Correctly close context before attempting migration --- osu.Game/Database/RealmContextFactory.cs | 17 +++++++++++------ osu.Game/OsuGameBase.cs | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index d7e35f736e..35ff91adb2 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -1,6 +1,3 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - using System; using System.Threading; using osu.Framework.Graphics; @@ -77,7 +74,7 @@ namespace osu.Game.Database { base.Update(); - if (Context.Refresh()) + if (context?.Refresh() == true) refreshes.Value++; } @@ -107,8 +104,7 @@ namespace osu.Game.Database { base.Dispose(isDisposing); - context?.Dispose(); - context = null; + FlushConnections(); } /// @@ -152,5 +148,14 @@ namespace osu.Game.Database pending_writes.Value--; } } + + public void FlushConnections() + { + var previousContext = context; + context = null; + previousContext?.Dispose(); + while (previousContext?.IsClosed == false) + Thread.Sleep(50); + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 759be79045..4bd4a6ae7f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -513,6 +513,7 @@ namespace osu.Game public void Migrate(string path) { contextFactory.FlushConnections(); + realmFactory.FlushConnections(); (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); } } From 47a9d2b1c2c49fe9c8bdb9024c00857786469734 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 20:53:16 +0900 Subject: [PATCH 077/107] Add missing licence header --- osu.Game/Database/RealmContextFactory.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 35ff91adb2..0ff4f857b9 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; using System.Threading; using osu.Framework.Graphics; From d480aa0e42187b35f819b7e6ad9b44baf1bf6579 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 22:57:55 +0900 Subject: [PATCH 078/107] Don't check for all ignored files being present in original folder (the realm exception is platform dependent) --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 045246e5ed..b0f9768e09 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -142,7 +142,8 @@ namespace osu.Game.Tests.NonVisual foreach (var file in osuStorage.IgnoreFiles) { - Assert.That(File.Exists(Path.Combine(originalDirectory, file))); + if (file.EndsWith(".ini", StringComparison.Ordinal)) + Assert.That(File.Exists(Path.Combine(originalDirectory, file))); Assert.That(storage.Exists(file), Is.False); } From d69a4914e05f5f5d1c2802fcad8551f9c1fe52c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 17:28:47 +0900 Subject: [PATCH 079/107] Add method to block all realm access during migration operation --- osu.Game/Database/IRealmFactory.cs | 2 +- osu.Game/Database/RealmContextFactory.cs | 87 ++++++++++++++----- osu.Game/OsuGameBase.cs | 8 +- .../KeyBinding/KeyBindingsSubsection.cs | 4 +- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 4a92a5683b..025c44f440 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -15,7 +15,7 @@ namespace osu.Game.Database /// /// Get a fresh context for read usage. /// - Realm GetForRead(); + RealmContextFactory.RealmUsage GetForRead(); /// /// Request a context for write usage. diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0ff4f857b9..c5d0061143 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -3,6 +3,7 @@ using System; using System.Threading; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; @@ -30,6 +31,9 @@ namespace osu.Game.Database private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); + private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages"); + + private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true); private Realm context; @@ -57,10 +61,10 @@ namespace osu.Game.Database this.storage = storage; } - public Realm GetForRead() + public RealmUsage GetForRead() { reads.Value++; - return createContext(); + return new RealmUsage(this); } public RealmWriteUsage GetForWrite() @@ -83,6 +87,8 @@ namespace osu.Game.Database private Realm createContext() { + blockingResetEvent.Wait(); + contexts_created.Value++; return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) @@ -107,24 +113,69 @@ namespace osu.Game.Database { base.Dispose(isDisposing); - FlushConnections(); + BlockAllOperations(); + } + + public IDisposable BlockAllOperations() + { + blockingResetEvent.Reset(); + flushContexts(); + + return new InvokeOnDisposal(this, r => endBlockingSection()); + } + + private void endBlockingSection() + { + blockingResetEvent.Set(); + } + + private void flushContexts() + { + var previousContext = context; + context = null; + + // wait for all threaded usages to finish + while (active_usages.Value > 0) + Thread.Sleep(50); + + previousContext?.Dispose(); + } + + /// + /// A usage of realm from an arbitrary thread. + /// + public class RealmUsage : IDisposable + { + public readonly Realm Realm; + + protected readonly RealmContextFactory Factory; + + internal RealmUsage(RealmContextFactory factory) + { + Factory = factory; + Realm = factory.createContext(); + } + + /// + /// Disposes this instance, calling the initially captured action. + /// + public virtual void Dispose() + { + Realm?.Dispose(); + active_usages.Value--; + } } /// /// A transaction used for making changes to realm data. /// - public class RealmWriteUsage : IDisposable + public class RealmWriteUsage : RealmUsage { - public readonly Realm Realm; - - private readonly RealmContextFactory factory; private readonly Transaction transaction; internal RealmWriteUsage(RealmContextFactory factory) + : base(factory) { - this.factory = factory; - - Realm = factory.createContext(); transaction = Realm.BeginWrite(); } @@ -141,24 +192,16 @@ namespace osu.Game.Database /// /// Disposes this instance, calling the initially captured action. /// - public virtual void Dispose() + public override void Dispose() { // rollback if not explicitly committed. transaction?.Dispose(); - Realm?.Dispose(); - Monitor.Exit(factory.writeLock); + base.Dispose(); + + Monitor.Exit(Factory.writeLock); pending_writes.Value--; } } - - public void FlushConnections() - { - var previousContext = context; - context = null; - previousContext?.Dispose(); - while (previousContext?.IsClosed == false) - Thread.Sleep(50); - } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4bd4a6ae7f..7806a38cd9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -512,9 +512,11 @@ namespace osu.Game public void Migrate(string path) { - contextFactory.FlushConnections(); - realmFactory.FlushConnections(); - (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + using (realmFactory.BlockAllOperations()) + { + contextFactory.FlushConnections(); + (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + } } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index 1cd600a72d..ac77440dfa 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -39,8 +39,8 @@ namespace osu.Game.Overlays.KeyBinding List bindings; - using (var realm = realmFactory.GetForRead()) - bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + using (var usage = realmFactory.GetForRead()) + bindings = usage.Realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { From 4d976094d1c69613ef581828d638ffdb04a48984 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Mar 2021 20:07:53 +0900 Subject: [PATCH 080/107] Switch Guid implementation temporarily to avoid compile time error --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index d10cb6af83..9abb749328 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -12,7 +12,14 @@ namespace osu.Game.Input.Bindings public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] - public Guid ID { get; set; } + public string StringGuid { get; set; } + + [Ignored] + public Guid ID + { + get => Guid.Parse(StringGuid); + set => StringGuid = value.ToString(); + } public int? RulesetID { get; set; } From 015cf5f7eba34442e07fd888af802443e3999075 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 13:22:48 +0900 Subject: [PATCH 081/107] Fix tests using wrong ID lookup type --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 0f9505beda..87a6fe00cc 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -131,7 +131,7 @@ namespace osu.Game.Overlays.KeyBinding using (var usage = realmFactory.GetForWrite()) { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID.ToString()); binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; usage.Commit(); @@ -296,7 +296,7 @@ namespace osu.Game.Overlays.KeyBinding { using (var write = realmFactory.GetForWrite()) { - var binding = write.Realm.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID); + var binding = write.Realm.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID.ToString()); binding.KeyCombinationString = bindTarget.KeyBinding.KeyCombinationString; write.Commit(); From 1281273dd32d7fb47ad4e943a46d7d8099b8b7e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 22:46:20 +0900 Subject: [PATCH 082/107] Add back automapper dependency --- osu.Game/osu.Game.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 7d4ae6ff8f..97ae793f7f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,6 +18,7 @@ + From 37bf79e8a412d88b4796627b4447d78458bd56af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 15:10:03 +0900 Subject: [PATCH 083/107] Remove unused automapper setup for the time being --- osu.Game/Database/RealmExtensions.cs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index 04b0820dd9..aee36e81c5 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -3,13 +3,7 @@ using System.Collections.Generic; using AutoMapper; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Input.Bindings; -using osu.Game.IO; -using osu.Game.Rulesets; -using osu.Game.Scoring; -using osu.Game.Skinning; using Realms; namespace osu.Game.Database @@ -21,22 +15,7 @@ namespace osu.Game.Database c.ShouldMapField = fi => false; c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - - c.CreateMap() - .ForMember(s => s.Beatmaps, d => d.MapFrom(s => s.Beatmaps)) - .ForMember(s => s.Files, d => d.MapFrom(s => s.Files)) - .MaxDepth(2); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); - c.CreateMap(); }).CreateMapper(); /// From ecde6137e0f907644eb426b37268de6e07d7cf31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 15:16:01 +0900 Subject: [PATCH 084/107] Add missing active usage counter increment --- osu.Game/Database/RealmContextFactory.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c5d0061143..ed5931dd2b 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -152,6 +152,7 @@ namespace osu.Game.Database internal RealmUsage(RealmContextFactory factory) { + active_usages.Value++; Factory = factory; Realm = factory.createContext(); } From 7e73af1c5a1a6a95b7d54c208e0de84b612015bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 31 Mar 2021 23:39:23 +0200 Subject: [PATCH 085/107] Revert "Suppress nuget warning due to including beta realm" This reverts commit cd8401c39c591955da4d2be52d4182dea6b6aa3a. Suppression is no longer necessary, as a normal realm release is used now. --- Directory.Build.props | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a91d423043..53ad973e47 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,16 +33,12 @@ DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 - NU5104: - This is triggered on osu.Game due to using a beta/prerelease version of realm. - Warning suppression can be removed after migrating off of a beta release. - CA9998: Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. The entire package will be able to be removed after migrating to .NET 5, as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;NU5104;CA9998 + $(NoWarn);NU1701;CA9998 false From cc9db90d11f6f6d3f0882d194efeaaaa4e696484 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 3 Apr 2021 18:58:25 +0900 Subject: [PATCH 086/107] Extract common implementation into private method --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 87a6fe00cc..767852896b 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -129,13 +129,7 @@ namespace osu.Game.Overlays.KeyBinding var button = buttons[i++]; button.UpdateKeyCombination(d); - using (var usage = realmFactory.GetForWrite()) - { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID.ToString()); - binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; - - usage.Commit(); - } + updateStoreFromButton(button); } } @@ -294,13 +288,7 @@ namespace osu.Game.Overlays.KeyBinding { if (bindTarget != null) { - using (var write = realmFactory.GetForWrite()) - { - var binding = write.Realm.Find(((IHasGuidPrimaryKey)bindTarget.KeyBinding).ID.ToString()); - binding.KeyCombinationString = bindTarget.KeyBinding.KeyCombinationString; - - write.Commit(); - } + updateStoreFromButton(bindTarget); bindTarget.IsBinding = false; Schedule(() => @@ -345,6 +333,17 @@ namespace osu.Game.Overlays.KeyBinding if (bindTarget != null) bindTarget.IsBinding = true; } + private void updateStoreFromButton(KeyButton button) + { + using (var usage = realmFactory.GetForWrite()) + { + var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID.ToString()); + binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; + + usage.Commit(); + } + } + private class CancelButton : TriangleButton { public CancelButton() From f9603eefe5fed7e02c040a0e12a4b27541e4aa60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Apr 2021 01:59:55 +0900 Subject: [PATCH 087/107] Revert "Switch Guid implementation temporarily to avoid compile time error" This reverts commit 4d976094d1c69613ef581828d638ffdb04a48984. --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 9abb749328..d10cb6af83 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -12,14 +12,7 @@ namespace osu.Game.Input.Bindings public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] - public string StringGuid { get; set; } - - [Ignored] - public Guid ID - { - get => Guid.Parse(StringGuid); - set => StringGuid = value.ToString(); - } + public Guid ID { get; set; } public int? RulesetID { get; set; } From 0fce0a420463f80b111cb9f976db6fdee7d1c3ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Apr 2021 02:04:42 +0900 Subject: [PATCH 088/107] Update to prerelease realm version --- osu.Game/FodyWeavers.xsd | 8 +++++++- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/FodyWeavers.xsd b/osu.Game/FodyWeavers.xsd index f526bddb09..447878c551 100644 --- a/osu.Game/FodyWeavers.xsd +++ b/osu.Game/FodyWeavers.xsd @@ -5,7 +5,13 @@ - + + + + Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/master/Realm/Realm.Fody/Common/Analytics.cs + + + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3bc0874b2d..81b89d587c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -32,7 +32,7 @@ - + From 6dd48f204c2dec42e6afb837bab65b06a15eb39e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Apr 2021 02:05:53 +0900 Subject: [PATCH 089/107] Remove unused store resolution --- osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 98b86d0b82..5f500d3023 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -26,9 +26,6 @@ namespace osu.Game.Input.Bindings private IDisposable realmSubscription; private IQueryable realmKeyBindings; - [Resolved] - private RealmKeyBindingStore store { get; set; } - [Resolved] private RealmContextFactory realmFactory { get; set; } From b9ee63ff891da7ce69245fc3f3bf66177064ef72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 Apr 2021 02:13:29 +0900 Subject: [PATCH 090/107] Remove `public` keywords from interface implementations --- osu.Game/Database/IHasGuidPrimaryKey.cs | 2 +- osu.Game/Database/IRealmFactory.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index ca41d70210..c9cd9b257a 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -11,6 +11,6 @@ namespace osu.Game.Database { [JsonIgnore] [PrimaryKey] - public Guid ID { get; set; } + Guid ID { get; set; } } } diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 025c44f440..c79442134c 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -10,7 +10,7 @@ namespace osu.Game.Database /// /// The main realm context, bound to the update thread. /// - public Realm Context { get; } + Realm Context { get; } /// /// Get a fresh context for read usage. From 311cfe04bbc8842f98ad08c00a46dbf63fbfde2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 17:20:33 +0100 Subject: [PATCH 091/107] Suppress nuget warning due to including beta realm --- Directory.Build.props | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 53ad973e47..a91d423043 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,12 +33,16 @@ DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 + NU5104: + This is triggered on osu.Game due to using a beta/prerelease version of realm. + Warning suppression can be removed after migrating off of a beta release. + CA9998: Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. The entire package will be able to be removed after migrating to .NET 5, as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;CA9998 + $(NoWarn);NU1701;NU5104;CA9998 false From 3bf462e4fad1855a538417171d7b1ffdd17dff7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 15:35:26 +0900 Subject: [PATCH 092/107] Add ignore rule for migrations for client.realm.lock --- osu.Game/IO/OsuStorage.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 1ed9a80a80..75130b0f9b 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -43,7 +43,8 @@ namespace osu.Game.IO { "framework.ini", "storage.ini", - "client.realm.note" + "client.realm.note", + "client.realm.lock", }; public OsuStorage(GameHost host, Storage defaultStorage) From 2c1422b4f90fd1abaa3552cd19730491d4b6b6b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 15:37:19 +0900 Subject: [PATCH 093/107] Add comment regarding teste edge case --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 6796344f24..a540ad7247 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -142,6 +142,8 @@ namespace osu.Game.Tests.NonVisual foreach (var file in osuStorage.IgnoreFiles) { + // avoid touching realm files which may be a pipe and break everything. + // this is also done locally inside OsuStorage via the IgnoreFiles list. if (file.EndsWith(".ini", StringComparison.Ordinal)) Assert.That(File.Exists(Path.Combine(originalDirectory, file))); Assert.That(storage.Exists(file), Is.False); From 8961203f0858b883783966a9b35ac6ce405d4cf9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:06:03 +0900 Subject: [PATCH 094/107] Move guid initialisation to database model itself --- osu.Game/Input/Bindings/RealmKeyBinding.cs | 2 +- osu.Game/Input/RealmKeyBindingStore.cs | 1 - osu.Game/OsuGameBase.cs | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index d10cb6af83..334d2da427 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -12,7 +12,7 @@ namespace osu.Game.Input.Bindings public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding { [PrimaryKey] - public Guid ID { get; set; } + public Guid ID { get; set; } = Guid.NewGuid(); public int? RulesetID { get; set; } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 76e65c1c70..ca830286df 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -74,7 +74,6 @@ namespace osu.Game.Input // insert any defaults which are missing. usage.Realm.Add(new RealmKeyBinding { - ID = Guid.NewGuid(), KeyCombinationString = k.KeyCombination.ToString(), ActionInt = (int)k.Action, RulesetID = rulesetId, diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0bb4d57ec3..c0796f41a0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -370,7 +370,6 @@ namespace osu.Game { usage.Realm.Add(new RealmKeyBinding { - ID = Guid.NewGuid(), KeyCombinationString = dkb.KeyCombination.ToString(), ActionInt = (int)dkb.Action, RulesetID = dkb.RulesetID, From 61f3cc9bc2cc843f50e6b1548a3e14929619e494 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:10:28 +0900 Subject: [PATCH 095/107] Fix update method not switched across to using `Guid` --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 767852896b..d98fea8eec 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -337,7 +337,7 @@ namespace osu.Game.Overlays.KeyBinding { using (var usage = realmFactory.GetForWrite()) { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID.ToString()); + var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; usage.Commit(); From 253c66034d05011838b1a8bfc994e49517a9fbd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:45:43 +0900 Subject: [PATCH 096/107] Remove unused using statement --- osu.Game/Input/RealmKeyBindingStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index ca830286df..547c79c209 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Input.Bindings; From 21b6adbf791a213c581aafddcd12a3da3d2ac19f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 17:52:17 +0900 Subject: [PATCH 097/107] Remove DI caching of `RealmKeyBindingStore` --- osu.Game/OsuGameBase.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index c0796f41a0..1e0c8f1332 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -290,8 +290,6 @@ namespace osu.Game migrateDataToRealm(); - dependencies.CacheAs(KeyBindingStore = new RealmKeyBindingStore(realmFactory)); - dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); From 9770c316e2bab93008785cfb880301f99e7a323c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 18:24:28 +0900 Subject: [PATCH 098/107] Add back the construction of the `KeyBindingStore` This reverts commit 21b6adbf791a213c581aafddcd12a3da3d2ac19f. --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1e0c8f1332..4c5920c616 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -337,6 +337,7 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); + KeyBindingStore = new RealmKeyBindingStore(realmFactory); KeyBindingStore.Register(globalBindings); foreach (var r in RulesetStore.AvailableRulesets) From 5f6fd9ba13a81a4f308a19ab3021e2bdaa525b26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Apr 2021 19:07:42 +0900 Subject: [PATCH 099/107] Remove outdated dependency requirement in test --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index bcad8f2d3c..4e5e8517a4 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual typeof(FileStore), typeof(ScoreManager), typeof(BeatmapManager), - typeof(RealmKeyBindingStore), typeof(SettingsStore), typeof(RulesetConfigCache), typeof(OsuColour), From 2a87b3d74bdf0bbecaea497d6992fb6760b667d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 13:43:55 +0900 Subject: [PATCH 100/107] Update realm package to latest beta version --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c2a3f997ce..6af7d82fdb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 9563b73ea645615630ab87daccfc806432f3af9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 14:13:58 +0900 Subject: [PATCH 101/107] Remove unnecessary using statement --- osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index d6ca839485..00180eae60 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Database; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings; From a7f50c5da66be62db20718ed540c5c7821735937 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 May 2021 17:13:25 +0900 Subject: [PATCH 102/107] Revert "Update realm package to latest beta version" This reverts commit 2a87b3d74bdf0bbecaea497d6992fb6760b667d5. --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6af7d82fdb..c2a3f997ce 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 9d168b19c9c081af3f3daac920917e60a105e59d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Jun 2021 02:15:25 +0900 Subject: [PATCH 103/107] Switch to non-beta release --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9c9d2e1a82..9f6bf0d2f4 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From d06e52505a9b8105a8683ea31506a96f69528d26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:01:51 +0900 Subject: [PATCH 104/107] Fix thread safety of `KeyBindingStore.GetReadableKeyCombinationsFor` --- osu.Game/Input/RealmKeyBindingStore.cs | 21 ++++++++++++++------- osu.Game/OsuGame.cs | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index e2efd546e7..45b7cf355f 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -26,16 +26,23 @@ namespace osu.Game.Input /// /// The action to lookup. /// A set of display strings for all the user's key configuration for the action. - public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) + public IReadOnlyList GetReadableKeyCombinationsFor(GlobalAction globalAction) { - foreach (var action in realmFactory.Context.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) - { - string str = action.KeyCombination.ReadableString(); + List combinations = new List(); - // even if found, the readable string may be empty for an unbound action. - if (str.Length > 0) - yield return str; + using (var context = realmFactory.GetForRead()) + { + foreach (var action in context.Realm.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) + { + string str = action.KeyCombination.ReadableString(); + + // even if found, the readable string may be empty for an unbound action. + if (str.Length > 0) + combinations.Add(str); + } } + + return combinations; } /// diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0c4d035728..907794d3bb 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -608,9 +608,9 @@ namespace osu.Game LocalConfig.LookupKeyBindings = l => { - var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l).ToArray(); + var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); - if (combinations.Length == 0) + if (combinations.Count == 0) return "none"; return string.Join(" or ", combinations); From d5a1524eb0e0d1d530f7cacbc1557faad1358b34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:12:01 +0900 Subject: [PATCH 105/107] Add missing `rulesetID` check for global action matching --- osu.Game/Input/RealmKeyBindingStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 45b7cf355f..9089169877 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -32,7 +32,7 @@ namespace osu.Game.Input using (var context = realmFactory.GetForRead()) { - foreach (var action in context.Realm.All().Where(b => (GlobalAction)b.ActionInt == globalAction)) + foreach (var action in context.Realm.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) { string str = action.KeyCombination.ReadableString(); From 5c59195e35b422b7e2481e529b9f43c4ce6bc153 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:34:45 +0900 Subject: [PATCH 106/107] Remove beta package allowance --- Directory.Build.props | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a91d423043..53ad973e47 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,16 +33,12 @@ DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. This is required due to https://github.com/NuGet/Home/issues/5740 - NU5104: - This is triggered on osu.Game due to using a beta/prerelease version of realm. - Warning suppression can be removed after migrating off of a beta release. - CA9998: Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated. The entire package will be able to be removed after migrating to .NET 5, as analysers are shipped as part of the .NET 5 SDK anyway. --> - $(NoWarn);NU1701;NU5104;CA9998 + $(NoWarn);NU1701;CA9998 false From 36b5414b1d83693cbc5049a6a67d3ded89f87295 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:45:30 +0900 Subject: [PATCH 107/107] Update comment to hopefully explain a weird conditional better --- osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 5f500d3023..10376c1866 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -83,8 +83,9 @@ namespace osu.Game.Input.Bindings var defaults = DefaultKeyBindings.ToList(); if (ruleset != null && !ruleset.ID.HasValue) - // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings. - // fallback to defaults instead. + // some tests instantiate a ruleset which is not present in the database. + // in these cases we still want key bindings to work, but matching to database instances would result in none being present, + // so let's populate the defaults directly. KeyBindings = defaults; else {