From ee103c9a1e4804744bed343d7a647f2d8e6a023f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 16:59:01 +0900 Subject: [PATCH 001/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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/173] 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 117e94bc94c2094d403cb0518f6fb5ecb2758b64 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 15:58:17 +0900 Subject: [PATCH 104/173] Allow setting `Entry` of `PoolableDrawableWithLifetime` It is more convenient than using the constructor because the only limited kind of expression is allowed in a base constructor call. Also, the object initializer syntax can be used. --- .../Objects/Drawables/DrawableHitObject.cs | 7 ++-- .../Pooling/PoolableDrawableWithLifetime.cs | 35 ++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7fc35fc778..a0717ec38e 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -156,10 +156,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// If null, a hitobject is expected to be later applied via (or automatically via pooling). /// protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null) - : base(initialHitObject != null ? new SyntheticHitObjectEntry(initialHitObject) : null) { - if (Entry != null) - ensureEntryHasResult(); + if (initialHitObject == null) return; + + Entry = new SyntheticHitObjectEntry(initialHitObject); + ensureEntryHasResult(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index 4440ca8d21..d5d1a7b55c 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; +using osu.Framework.Graphics; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; @@ -16,14 +17,32 @@ namespace osu.Game.Rulesets.Objects.Pooling /// The type storing state and controlling this drawable. public abstract class PoolableDrawableWithLifetime : PoolableDrawable where TEntry : LifetimeEntry { + private TEntry? entry; + /// /// The entry holding essential state of this . /// - public TEntry? Entry { get; private set; } + /// + /// If a non-null value is set before loading is started, the entry is applied when the loading is completed. + /// + public TEntry? Entry + { + get => entry; + + set + { + if (LoadState < LoadState.Ready) + entry = value; + else if (value != null) + Apply(value); + else if (HasEntryApplied) + free(); + } + } /// /// Whether is applied to this . - /// When an initial entry is specified in the constructor, is set but not applied until loading is completed. + /// When an is set during initialization, it is not applied until loading is completed. /// protected bool HasEntryApplied { get; private set; } @@ -65,7 +84,7 @@ namespace osu.Game.Rulesets.Objects.Pooling { base.LoadAsyncComplete(); - // Apply the initial entry given in the constructor. + // Apply the initial entry. if (Entry != null && !HasEntryApplied) Apply(Entry); } @@ -79,7 +98,7 @@ namespace osu.Game.Rulesets.Objects.Pooling if (HasEntryApplied) free(); - Entry = entry; + this.entry = entry; entry.LifetimeChanged += setLifetimeFromEntry; setLifetimeFromEntry(entry); @@ -113,12 +132,12 @@ namespace osu.Game.Rulesets.Objects.Pooling private void free() { - Debug.Assert(Entry != null && HasEntryApplied); + Debug.Assert(entry != null && HasEntryApplied); - OnFree(Entry); + OnFree(entry); - Entry.LifetimeChanged -= setLifetimeFromEntry; - Entry = null; + entry.LifetimeChanged -= setLifetimeFromEntry; + entry = null; base.LifetimeStart = double.MinValue; base.LifetimeEnd = double.MaxValue; From 55859938b183c950b94625c552c66478a6ec3168 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 16 Jun 2021 16:01:29 +0900 Subject: [PATCH 105/173] Use object initializer syntax for hit object application in tests --- .../Gameplay/TestSceneDrawableHitObject.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index da0d57f9d1..0ce71696bd 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -44,11 +44,9 @@ namespace osu.Game.Tests.Gameplay { TestDrawableHitObject dho = null; TestLifetimeEntry entry = null; - AddStep("Create DHO", () => + AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject { - dho = new TestDrawableHitObject(null); - dho.Apply(entry = new TestLifetimeEntry(new HitObject())); - Child = dho; + Entry = entry = new TestLifetimeEntry(new HitObject()) }); AddStep("KeepAlive = true", () => @@ -81,12 +79,10 @@ namespace osu.Game.Tests.Gameplay AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET); TestDrawableHitObject dho = null; - AddStep("Create DHO", () => + AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject { - dho = new TestDrawableHitObject(null); - dho.Apply(entry); - Child = dho; - dho.SetLifetimeStartOnApply = true; + Entry = entry, + SetLifetimeStartOnApply = true }); AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY); @@ -97,11 +93,9 @@ namespace osu.Game.Tests.Gameplay { TestDrawableHitObject dho = null; TestLifetimeEntry entry = null; - AddStep("Create DHO", () => + AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject { - dho = new TestDrawableHitObject(null); - dho.Apply(entry = new TestLifetimeEntry(new HitObject())); - Child = dho; + Entry = entry = new TestLifetimeEntry(new HitObject()) }); AddStep("Set entry lifetime", () => @@ -135,7 +129,7 @@ namespace osu.Game.Tests.Gameplay public bool SetLifetimeStartOnApply; - public TestDrawableHitObject(HitObject hitObject) + public TestDrawableHitObject(HitObject hitObject = null) : base(hitObject) { } From 4b926791b58344f6018941ac6463849bc742bdb5 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Wed, 16 Jun 2021 21:13:01 +0700 Subject: [PATCH 106/173] add inter font --- osu.Game/OsuGameBase.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9c3adba342..abf8fbc4fb 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -195,6 +195,15 @@ namespace osu.Game AddFont(Resources, @"Fonts/Torus-SemiBold"); AddFont(Resources, @"Fonts/Torus-Bold"); + AddFont(Resources, @"Fonts/Inter-Regular"); + AddFont(Resources, @"Fonts/Inter-RegularItalic"); + AddFont(Resources, @"Fonts/Inter-Light"); + AddFont(Resources, @"Fonts/Inter-LightItalic"); + AddFont(Resources, @"Fonts/Inter-SemiBold"); + AddFont(Resources, @"Fonts/Inter-SemiBoldItalic"); + AddFont(Resources, @"Fonts/Inter-Bold"); + AddFont(Resources, @"Fonts/Inter-BoldItalic"); + AddFont(Resources, @"Fonts/Noto-Basic"); AddFont(Resources, @"Fonts/Noto-Hangul"); AddFont(Resources, @"Fonts/Noto-CJK-Basic"); From 0e9ca3df3c5b8a54d9808711bacc926e1e832183 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Wed, 16 Jun 2021 21:14:48 +0700 Subject: [PATCH 107/173] add remaining non-rtl language --- osu.Game/Localisation/Language.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index 96bfde8596..65541feedf 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -14,9 +14,8 @@ namespace osu.Game.Localisation // [Description(@"اَلْعَرَبِيَّةُ")] // ar, - // TODO: Some accented glyphs are missing. Revisit when adding Inter. - // [Description(@"Беларуская мова")] - // be, + [Description(@"Беларуская мова")] + be, [Description(@"Български")] bg, @@ -30,9 +29,8 @@ namespace osu.Game.Localisation [Description(@"Deutsch")] de, - // TODO: Some accented glyphs are missing. Revisit when adding Inter. - // [Description(@"Ελληνικά")] - // el, + [Description(@"Ελληνικά")] + el, [Description(@"español")] es, @@ -94,9 +92,8 @@ namespace osu.Game.Localisation [Description(@"Türkçe")] tr, - // TODO: Some accented glyphs are missing. Revisit when adding Inter. - // [Description(@"Українська мова")] - // uk, + [Description(@"Українська мова")] + uk, [Description(@"Tiếng Việt")] vi, From e7954ecb606e53e88063a227168d2d3fc5623ab6 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 17 Jun 2021 10:31:20 +0900 Subject: [PATCH 108/173] Use property instead of backing field consistently --- .../Objects/Pooling/PoolableDrawableWithLifetime.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index d5d1a7b55c..3ab85aa214 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -132,11 +132,11 @@ namespace osu.Game.Rulesets.Objects.Pooling private void free() { - Debug.Assert(entry != null && HasEntryApplied); + Debug.Assert(Entry != null && HasEntryApplied); - OnFree(entry); + OnFree(Entry); - entry.LifetimeChanged -= setLifetimeFromEntry; + Entry.LifetimeChanged -= setLifetimeFromEntry; entry = null; base.LifetimeStart = double.MinValue; base.LifetimeEnd = double.MaxValue; From 9d9892e99ed8290f0b5ad8d82e800efa84935473 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Jun 2021 04:38:55 +0300 Subject: [PATCH 109/173] Add legacy spinner approach circle implementation --- .../Skinning/Legacy/LegacySpinner.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 959589620b..259f16ca5e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } + private Drawable approachCircle; + private Sprite spin; private Sprite clear; @@ -57,8 +59,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Depth = float.MinValue, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { + approachCircle = getSpinnerApproachCircle(source), spin = new Sprite { Anchor = Anchor.TopCentre, @@ -101,6 +104,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy }.With(s => s.Font = s.Font.With(fixedWidth: false)), } }); + + static Drawable getSpinnerApproachCircle(ISkinSource source) + { + var spinnerProvider = source.FindProvider(s => + s.GetTexture("spinner-circle") != null || + s.GetTexture("spinner-top") != null); + + if (spinnerProvider is DefaultLegacySkin) + return Empty(); + + return new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-approachcircle"), + Scale = new Vector2(SPRITE_SCALE * 1.86f), + Y = SPINNER_Y_CENTRE, + }; + } } private IBindable gainedBonus; @@ -175,6 +197,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); } + using (BeginAbsoluteSequence(d.HitObject.StartTime)) + approachCircle.ScaleTo(SPRITE_SCALE * 1.86f).ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration); + double spinFadeOutLength = Math.Min(400, d.HitObject.Duration); using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true)) From c9458fd9cec36e9d9407f4210fa39bf9bbef4ab1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Jun 2021 04:57:29 +0300 Subject: [PATCH 110/173] Hide spinner approach circle in "Hidden" mod --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 3 +++ osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 7 +++++++ osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 2752feb0a1..bec4c62e0f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -110,6 +110,9 @@ namespace osu.Game.Rulesets.Osu.Mods // hide elements we don't care about. // todo: hide background + using (spinner.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt)) + spinner.HideApproachCircle(); + using (spinner.BeginAbsoluteSequence(fadeStartTime)) spinner.FadeOut(fadeDuration); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 19cee61f26..942cc52c50 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; + internal readonly Bindable ApproachCircleVisibility = new Bindable(Visibility.Visible); + /// /// The amount of bonus score gained from spinning after the required number of spins, for display purposes. /// @@ -285,6 +287,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables updateBonusScore(); } + /// + /// Hides the spinner's approach circle if it has one. + /// + public void HideApproachCircle() => this.TransformBindableTo(ApproachCircleVisibility, Visibility.Hidden); + private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; private int wholeSpins; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 259f16ca5e..5b0c2d405b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -125,6 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy } } + private IBindable approachCircleVisibility; private IBindable gainedBonus; private IBindable spinsPerMinute; @@ -134,6 +135,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { base.LoadComplete(); + approachCircleVisibility = DrawableSpinner.ApproachCircleVisibility.GetBoundCopy(); + approachCircleVisibility.BindValueChanged(v => + { + approachCircle.Alpha = v.NewValue == Visibility.Hidden ? 0 : 1; + }, true); + gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy(); gainedBonus.BindValueChanged(bonus => { From 5933e0d2d99a30ebc57b83a8dad81934746eabc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 13:17:32 +0900 Subject: [PATCH 111/173] Change `CheckCompatibleSet` to never deselect the current candidat when checking incompatibility --- osu.Game.Tests/Mods/ModUtilsTest.cs | 30 +++++++++++++++++++++++++++-- osu.Game/Utils/ModUtils.cs | 3 +++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 7384471c41..9f27289d7e 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -21,6 +21,14 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); } + [Test] + public void TestModIsCompatibleByItselfWithIncompatibleInterface() + { + var mod = new Mock(); + mod.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) }); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); + } + [Test] public void TestIncompatibleThroughTopLevel() { @@ -34,6 +42,20 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False); } + [Test] + public void TestIncompatibleThroughInterface() + { + var mod1 = new Mock(); + var mod2 = new Mock(); + + mod1.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) }); + mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) }); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False); + } + [Test] public void TestMultiModIncompatibleWithTopLevel() { @@ -149,11 +171,15 @@ namespace osu.Game.Tests.Mods Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); } - public abstract class CustomMod1 : Mod + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } - public abstract class CustomMod2 : Mod + public abstract class CustomMod2 : Mod, IModCompatibilitySpecification + { + } + + public interface IModCompatibilitySpecification { } } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 1c3558fc90..98766cb844 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -60,6 +60,9 @@ namespace osu.Game.Utils { foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m))) { + if (invalid == mod) + continue; + invalidMods ??= new List(); invalidMods.Add(invalid); } From 4de27429bc0dc65f988c8b1e67941464ce840bb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 13:17:55 +0900 Subject: [PATCH 112/173] Change `ModSelectOverlay` to never deselect the user triggered selection --- osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs | 2 +- osu.Game/Overlays/Mods/ModSection.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs b/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs index 78cd9bdae5..db76581108 100644 --- a/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods base.OnModSelected(mod); foreach (var section in ModSectionsContainer.Children) - section.DeselectTypes(mod.IncompatibleMods, true); + section.DeselectTypes(mod.IncompatibleMods, true, mod); } } } diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index aa8a5efd39..6e289dc8aa 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -159,12 +159,16 @@ namespace osu.Game.Overlays.Mods /// /// The types of s which should be deselected. /// Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow. - public void DeselectTypes(IEnumerable modTypes, bool immediate = false) + /// If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in . + public void DeselectTypes(IEnumerable modTypes, bool immediate = false, Mod newSelection = null) { foreach (var button in Buttons) { if (button.SelectedMod == null) continue; + if (button.SelectedMod == newSelection) + continue; + foreach (var type in modTypes) { if (type.IsInstanceOfType(button.SelectedMod)) From 860626152aedc57dc941031e60067ae63c253181 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 12:54:40 +0900 Subject: [PATCH 113/173] Mark all mods which adjust approach circle as incompatible with each other Closes https://github.com/ppy/osu/issues/13543. --- osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs | 12 ++++++++++++ .../Mods/OsuModApproachDifferent.cs | 5 ++++- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 4 ++-- 6 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs diff --git a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs new file mode 100644 index 0000000000..60a5825241 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Mods +{ + /// + /// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes. + /// + public interface IMutateApproachCircles + { + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index 074fb7dbed..526e29ad53 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -11,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject + public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles { public override string Name => "Approach Different"; public override string Acronym => "AD"; @@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; + [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)] public BindableFloat Scale { get; } = new BindableFloat(4) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 2752feb0a1..a7c79aa2a0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -14,12 +14,12 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden + public class OsuModHidden : ModHidden, IMutateApproachCircles { public override string Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => 1.06; - public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index d1be162f73..6dfabed0df 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Adjusts the size of hit objects during their fade in animation. /// - public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment + public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles { public override ModType Type => ModType.Fun; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods protected virtual float EndScale => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index 96ba58da23..d3ca2973f0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpinIn : ModWithVisibilityAdjustment + public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles { public override string Name => "Spin In"; public override string Acronym => "SI"; @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; // todo: this mod should be able to be compatible with hidden with a bit of further implementation. - public override Type[] IncompatibleMods => new[] { typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; private const int rotate_offset = 360; private const float rotate_starting_width = 2; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 4b0939db16..84263221a7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModTraceable : ModWithVisibilityAdjustment + public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles { public override string Name => "Traceable"; public override string Acronym => "TC"; @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { From 5cf2ac78fcc1790b2556d11336c10496c7a94da8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 15:40:35 +0900 Subject: [PATCH 114/173] Adjust font namespaces --- osu.Game/OsuGameBase.cs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index abf8fbc4fb..05a53452f7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -190,29 +190,29 @@ namespace osu.Game AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Torus-Regular"); - AddFont(Resources, @"Fonts/Torus-Light"); - AddFont(Resources, @"Fonts/Torus-SemiBold"); - AddFont(Resources, @"Fonts/Torus-Bold"); + AddFont(Resources, @"Fonts/Torus/Torus-Regular"); + AddFont(Resources, @"Fonts/Torus/Torus-Light"); + AddFont(Resources, @"Fonts/Torus/Torus-SemiBold"); + AddFont(Resources, @"Fonts/Torus/Torus-Bold"); - AddFont(Resources, @"Fonts/Inter-Regular"); - AddFont(Resources, @"Fonts/Inter-RegularItalic"); - AddFont(Resources, @"Fonts/Inter-Light"); - AddFont(Resources, @"Fonts/Inter-LightItalic"); - AddFont(Resources, @"Fonts/Inter-SemiBold"); - AddFont(Resources, @"Fonts/Inter-SemiBoldItalic"); - AddFont(Resources, @"Fonts/Inter-Bold"); - AddFont(Resources, @"Fonts/Inter-BoldItalic"); + AddFont(Resources, @"Fonts/Inter/Inter-Regular"); + AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic"); + AddFont(Resources, @"Fonts/Inter/Inter-Light"); + AddFont(Resources, @"Fonts/Inter/Inter-LightItalic"); + AddFont(Resources, @"Fonts/Inter/Inter-SemiBold"); + AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic"); + AddFont(Resources, @"Fonts/Inter/Inter-Bold"); + AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic"); - AddFont(Resources, @"Fonts/Noto-Basic"); - AddFont(Resources, @"Fonts/Noto-Hangul"); - AddFont(Resources, @"Fonts/Noto-CJK-Basic"); - AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); - AddFont(Resources, @"Fonts/Noto-Thai"); + AddFont(Resources, @"Fonts/Noto/Noto-Basic"); + AddFont(Resources, @"Fonts/Noto/Noto-Hangul"); + AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic"); + AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility"); + AddFont(Resources, @"Fonts/Noto/Noto-Thai"); - AddFont(Resources, @"Fonts/Venera-Light"); - AddFont(Resources, @"Fonts/Venera-Bold"); - AddFont(Resources, @"Fonts/Venera-Black"); + AddFont(Resources, @"Fonts/Venera/Venera-Light"); + AddFont(Resources, @"Fonts/Venera/Venera-Bold"); + AddFont(Resources, @"Fonts/Venera/Venera-Black"); Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; From 2bf855fcca95d02d5a52746699a824ca9cebd5de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 15:45:12 +0900 Subject: [PATCH 115/173] Move all storyboard outro skip logic out of `updateCompletionState` This method should only be called to trigger the score completion portion of player progression. The storyboard skip/end logic is now handled separately in `progressToResults`. --- osu.Game/Screens/Play/Player.cs | 71 +++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9fe12c58de..c8ba554c5f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -295,12 +295,12 @@ namespace osu.Game.Screens.Play DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded => { - if (storyboardEnded.NewValue && resultsDisplayDelegate == null) - updateCompletionState(); + if (storyboardEnded.NewValue) + progressToResults(true); }; // Bind the judgement processors to ourselves - ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState()); + ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged); HealthProcessor.Failed += onFail; foreach (var mod in Mods.Value.OfType()) @@ -374,7 +374,7 @@ namespace osu.Game.Screens.Play }, skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0) { - RequestSkip = () => updateCompletionState(true), + RequestSkip = () => progressToResults(false), Alpha = 0 }, FailOverlay = new FailOverlay @@ -643,9 +643,8 @@ namespace osu.Game.Screens.Play /// /// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime. /// - /// If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it. /// Thrown if this method is called more than once without changing state. - private void updateCompletionState(bool skipStoryboardOutro = false) + private void scoreCompletionChanged(ValueChangedEvent completed) { // If this player instance is in the middle of an exit, don't attempt any kind of state update. if (!this.IsCurrentScreen()) @@ -656,7 +655,7 @@ namespace osu.Game.Screens.Play // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run). // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done, // but it still doesn't feel right that this exists here. - if (!ScoreProcessor.HasCompleted.Value) + if (!completed.NewValue) { resultsDisplayDelegate?.Cancel(); resultsDisplayDelegate = null; @@ -667,7 +666,7 @@ namespace osu.Game.Screens.Play } if (resultsDisplayDelegate != null) - throw new InvalidOperationException(@$"{nameof(updateCompletionState)} should never be fired more than once."); + throw new InvalidOperationException(@$"{nameof(scoreCompletionChanged)} should never be fired more than once."); // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed) @@ -686,22 +685,49 @@ namespace osu.Game.Screens.Play // Asynchronously run score preparation operations (database import, online submission etc.). prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); - if (skipStoryboardOutro) - { - scheduleCompletion(); - return; - } - bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; if (storyboardHasOutro) { + // if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending + // or the user pressing the skip outro button. skipOutroOverlay.Show(); return; } - using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) - scheduleCompletion(); + progressToResults(true); + } + + /// + /// Queue the results screen for display. + /// + /// + /// A final display will only occur once all work is completed in . + /// + /// Whether a minimum delay () should be added before the screen is displayed. + private void progressToResults(bool withDelay) + { + if (resultsDisplayDelegate != null) + return; + + double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; + + resultsDisplayDelegate = new ScheduledDelegate(() => + { + if (prepareScoreForDisplayTask?.IsCompleted != true) + // if the asynchronous preparation has not completed, keep repeating this delegate. + return; + + resultsDisplayDelegate?.Cancel(); + + if (!this.IsCurrentScreen()) + // This player instance may already be in the process of exiting. + return; + + this.Push(CreateResults(prepareScoreForDisplayTask.Result)); + }, Time.Current + delay, 50); + + Scheduler.Add(resultsDisplayDelegate); } private async Task prepareScoreForResults() @@ -734,7 +760,18 @@ namespace osu.Game.Screens.Play { if (!prepareScoreForDisplayTask.IsCompleted) { - scheduleCompletion(); + resultsDisplayDelegate = Schedule(() => + { + if (!prepareScoreForDisplayTask.IsCompleted) + { + scheduleCompletion(); + return; + } + + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(prepareScoreForDisplayTask.Result)); + }); return; } From 752d0a9f0be0d5fa18580adf1bcfbb505e587d23 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 16:08:14 +0900 Subject: [PATCH 116/173] add sound to scroll-to-top button --- osu.Game/Graphics/Containers/OsuHoverContainer.cs | 4 +++- osu.Game/Graphics/UserInterface/HoverSampleSet.cs | 5 ++++- osu.Game/Overlays/OverlayScrollContainer.cs | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs index 67af79c763..ac66fd658a 100644 --- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Input.Events; using osuTK.Graphics; using System.Collections.Generic; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.Containers { @@ -20,7 +21,8 @@ namespace osu.Game.Graphics.Containers protected virtual IEnumerable EffectTargets => new[] { Content }; - public OsuHoverContainer() + public OsuHoverContainer(HoverSampleSet sampleSet = HoverSampleSet.Default) + : base(sampleSet) { Enabled.ValueChanged += e => { diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index c74ac90a4c..d74d13af95 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -20,6 +20,9 @@ namespace osu.Game.Graphics.UserInterface Toolbar, [Description("songselect")] - SongSelect + SongSelect, + + [Description("scrolltotop")] + ScrollToTop } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 0004719b87..c5b4cc3645 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -84,6 +85,7 @@ namespace osu.Game.Overlays private readonly Box background; public ScrollToTopButton() + : base(HoverSampleSet.ScrollToTop) { Size = new Vector2(50); Alpha = 0; From 7ef8eac7737c3fb82364fef8575eb173bff34f6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 15:56:58 +0900 Subject: [PATCH 117/173] Remove unnecessary (and no longer correct) exception --- osu.Game/Screens/Play/Player.cs | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c8ba554c5f..5913e46836 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -665,9 +665,6 @@ namespace osu.Game.Screens.Play return; } - if (resultsDisplayDelegate != null) - throw new InvalidOperationException(@$"{nameof(scoreCompletionChanged)} should never be fired more than once."); - // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed) return; @@ -703,6 +700,7 @@ namespace osu.Game.Screens.Play /// /// /// A final display will only occur once all work is completed in . + /// This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes . /// /// Whether a minimum delay () should be added before the screen is displayed. private void progressToResults(bool withDelay) @@ -756,30 +754,6 @@ namespace osu.Game.Screens.Play return Score.ScoreInfo; } - private void scheduleCompletion() => resultsDisplayDelegate = Schedule(() => - { - if (!prepareScoreForDisplayTask.IsCompleted) - { - resultsDisplayDelegate = Schedule(() => - { - if (!prepareScoreForDisplayTask.IsCompleted) - { - scheduleCompletion(); - return; - } - - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(prepareScoreForDisplayTask.Result)); - }); - return; - } - - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(prepareScoreForDisplayTask.Result)); - }); - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; #region Fail Logic From 06d1bd971cb5fdc37788efd49f8b8375074bde0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:08:49 +0900 Subject: [PATCH 118/173] Default `DrawableStoryboard` to a completed state to avoid state change on empty storyboards --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index ca041da801..8a31e4576a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -25,7 +25,7 @@ namespace osu.Game.Storyboards.Drawables /// public IBindable HasStoryboardEnded => hasStoryboardEnded; - private readonly BindableBool hasStoryboardEnded = new BindableBool(); + private readonly BindableBool hasStoryboardEnded = new BindableBool(true); protected override Container Content { get; } From 3819a1f03b510759032f5dc5e967964dcd0bab52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:12:34 +0900 Subject: [PATCH 119/173] Remove exit override behaviour I don't actually know under what scenario this could have been hit, and actually caused expected behaviour. Consider that in the scenario I describe in the comment (which I added yesterday), the user is requesting a pause or exit which would be "cancelled showing the results instead". But in such a scenario, `PerformExit` would first be run, which cancels the `resultsDisplayDelegate` in the first place. The only special case would be pressing the close button on the window decoration? Which I don't think should be a special case in the first place, so I'm just going to remove this for the time being to keep things simple. --- osu.Game/Screens/Play/Player.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5913e46836..5db9b47586 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -950,14 +950,6 @@ namespace osu.Game.Screens.Play { screenSuspension?.Expire(); - // if the results screen is prepared to be displayed, forcefully show it on an exit request. - // usually if a user has completed a play session they do want to see results. and if they don't they can hit the same key a second time. - if (resultsDisplayDelegate != null && !resultsDisplayDelegate.Cancelled && !resultsDisplayDelegate.Completed) - { - resultsDisplayDelegate.RunTask(); - return true; - } - // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // To resolve test failures, forcefully end playing synchronously when this screen exits. // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. From f3426e38b422590a0f1f97ac096742254391bd9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:18:20 +0900 Subject: [PATCH 120/173] Add note about delay parameter --- osu.Game/Screens/Play/Player.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5db9b47586..f2d31e5dd2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -699,13 +699,19 @@ namespace osu.Game.Screens.Play /// Queue the results screen for display. /// /// - /// A final display will only occur once all work is completed in . - /// This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes . + /// A final display will only occur once all work is completed in . This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes . + /// + /// Calling this method multiple times will have no effect. /// /// Whether a minimum delay () should be added before the screen is displayed. private void progressToResults(bool withDelay) { if (resultsDisplayDelegate != null) + // Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be + // accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued + // may take x00 more milliseconds than expected in the very rare edge case). + // + // If required we can handle this more correctly by rescheduling here. return; double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; @@ -713,7 +719,7 @@ namespace osu.Game.Screens.Play resultsDisplayDelegate = new ScheduledDelegate(() => { if (prepareScoreForDisplayTask?.IsCompleted != true) - // if the asynchronous preparation has not completed, keep repeating this delegate. + // If the asynchronous preparation has not completed, keep repeating this delegate. return; resultsDisplayDelegate?.Cancel(); From 19507e107e7185609db5b71c817e842b0cd92909 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:46:40 +0900 Subject: [PATCH 121/173] Reorder methods to make more sense --- osu.Game/Screens/Play/Player.cs | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 76f5e2336c..eab3770e27 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -695,6 +695,29 @@ namespace osu.Game.Screens.Play progressToResults(true); } + private async Task prepareScoreForResults() + { + try + { + await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error(ex, @"Score preparation failed!"); + } + + try + { + await ImportScore(Score).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error(ex, @"Score import failed!"); + } + + return Score.ScoreInfo; + } + /// /// Queue the results screen for display. /// @@ -734,29 +757,6 @@ namespace osu.Game.Screens.Play Scheduler.Add(resultsDisplayDelegate); } - private async Task prepareScoreForResults() - { - try - { - await PrepareScoreForResultsAsync(Score).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Error(ex, @"Score preparation failed!"); - } - - try - { - await ImportScore(Score).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Error(ex, @"Score import failed!"); - } - - return Score.ScoreInfo; - } - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; #region Fail Logic From 0bc68a70181d1b09b5edbe59014d2a948a89c78b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 16:49:43 +0900 Subject: [PATCH 122/173] Move xmldoc to method --- osu.Game/Screens/Play/Player.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index eab3770e27..cadcc474b2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -679,7 +679,6 @@ namespace osu.Game.Screens.Play if (!Configuration.ShowResults) return; - // Asynchronously run score preparation operations (database import, online submission etc.). prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults); bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; @@ -695,6 +694,10 @@ namespace osu.Game.Screens.Play progressToResults(true); } + /// + /// Asynchronously run score preparation operations (database import, online submission etc.). + /// + /// The final score. private async Task prepareScoreForResults() { try From d06e52505a9b8105a8683ea31506a96f69528d26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 17:01:51 +0900 Subject: [PATCH 123/173] 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 124/173] 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 125/173] 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 126/173] 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 { From 4feb7c848f1e13e6030045c1fb06507fd0a58714 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 17:26:28 +0900 Subject: [PATCH 127/173] add sound to tab controls --- osu.Game/Graphics/UserInterface/HoverSampleSet.cs | 3 +++ osu.Game/Graphics/UserInterface/OsuTabControl.cs | 2 +- osu.Game/Graphics/UserInterface/PageTabControl.cs | 2 +- osu.Game/Overlays/OverlayTabControl.cs | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index d74d13af95..646131ed4f 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -22,6 +22,9 @@ namespace osu.Game.Graphics.UserInterface [Description("songselect")] SongSelect, + [Description("tabselect")] + TabSelect, + [Description("scrolltotop")] ScrollToTop } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 0c220336a5..c447d7f609 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -172,7 +172,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, }, - new HoverClickSounds() + new HoverClickSounds(HoverSampleSet.TabSelect) }; } diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index 1ba9ad53bb..a218c7bf52 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -76,7 +76,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, }, - new HoverClickSounds() + new HoverClickSounds(HoverSampleSet.TabSelect) }; Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs index a1cbf2c1e7..578cd703c7 100644 --- a/osu.Game/Overlays/OverlayTabControl.cs +++ b/osu.Game/Overlays/OverlayTabControl.cs @@ -99,7 +99,7 @@ namespace osu.Game.Overlays ExpandedSize = 5f, CollapsedSize = 0 }, - new HoverClickSounds() + new HoverClickSounds(HoverSampleSet.TabSelect) }; } From d462394635c67dbf24238fdccfb38916474f134a Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 18:57:40 +0900 Subject: [PATCH 128/173] add sound to dropdowns --- .../Graphics/UserInterface/HoverSampleSet.cs | 3 -- .../Graphics/UserInterface/OsuDropdown.cs | 29 ++++++++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index 646131ed4f..3a8187c8f4 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -13,9 +13,6 @@ namespace osu.Game.Graphics.UserInterface [Description("button")] Button, - [Description("softer")] - Soft, - [Description("toolbar")] Toolbar, diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 15fb00ccb0..24d062e059 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -4,6 +4,8 @@ using System.Linq; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -57,6 +59,9 @@ namespace osu.Game.Graphics.UserInterface { public override bool HandleNonPositionalInput => State == MenuState.Open; + private Sample sampleOpen; + private Sample sampleClose; + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring public OsuDropdownMenu() { @@ -69,9 +74,25 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding(5); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); + sampleClose = audio.Samples.Get(@"UI/dropdown-close"); + } + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring - protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint); - protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint); + protected override void AnimateOpen() + { + this.FadeIn(300, Easing.OutQuint); + sampleOpen?.Play(); + } + + protected override void AnimateClose() + { + this.FadeOut(300, Easing.OutQuint); + sampleClose?.Play(); + } // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring protected override void UpdateSize(Vector2 newSize) @@ -155,7 +176,7 @@ namespace osu.Game.Graphics.UserInterface nonAccentSelectedColour = Color4.Black.Opacity(0.5f); updateColours(); - AddInternal(new HoverClickSounds(HoverSampleSet.Soft)); + AddInternal(new HoverSounds()); } protected override void UpdateForegroundColour() @@ -262,7 +283,7 @@ namespace osu.Game.Graphics.UserInterface }, }; - AddInternal(new HoverClickSounds()); + AddInternal(new HoverSounds()); } [BackgroundDependencyLoader] From 36d2199a0241173a8931fab9f17ef56a2191970e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 19:20:57 +0900 Subject: [PATCH 129/173] Add exception on Apply() while loading --- .../Pooling/PoolableDrawableWithLifetime.cs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index 3ab85aa214..faa82786cd 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -28,12 +28,9 @@ namespace osu.Game.Rulesets.Objects.Pooling public TEntry? Entry { get => entry; - set { - if (LoadState < LoadState.Ready) - entry = value; - else if (value != null) + if (value != null) Apply(value); else if (HasEntryApplied) free(); @@ -86,7 +83,7 @@ namespace osu.Game.Rulesets.Objects.Pooling // Apply the initial entry. if (Entry != null && !HasEntryApplied) - Apply(Entry); + apply(Entry); } /// @@ -95,16 +92,10 @@ namespace osu.Game.Rulesets.Objects.Pooling /// public void Apply(TEntry entry) { - if (HasEntryApplied) - free(); + if (LoadState == LoadState.Loading) + throw new InvalidOperationException($"Cannot apply a new {nameof(TEntry)} while currently loading."); - this.entry = entry; - entry.LifetimeChanged += setLifetimeFromEntry; - setLifetimeFromEntry(entry); - - OnApply(entry); - - HasEntryApplied = true; + apply(entry); } protected sealed override void FreeAfterUse() @@ -130,6 +121,20 @@ namespace osu.Game.Rulesets.Objects.Pooling { } + private void apply(TEntry entry) + { + if (HasEntryApplied) + free(); + + this.entry = entry; + entry.LifetimeChanged += setLifetimeFromEntry; + setLifetimeFromEntry(entry); + + OnApply(entry); + + HasEntryApplied = true; + } + private void free() { Debug.Assert(Entry != null && HasEntryApplied); From 36d51d5117e8a149de7d057fcf9e116d395133bf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 19:23:37 +0900 Subject: [PATCH 130/173] Don't set entry immediately --- .../Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index faa82786cd..3b261494ff 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -30,7 +30,9 @@ namespace osu.Game.Rulesets.Objects.Pooling get => entry; set { - if (value != null) + if (LoadState == LoadState.NotLoaded) + entry = value; + else if (value != null) Apply(value); else if (HasEntryApplied) free(); From 42c5a962fbbc9a0ff871c387c6d8345104b45276 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Jun 2021 19:27:10 +0900 Subject: [PATCH 131/173] Add xmldoc remark --- .../Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index 3b261494ff..9c6097a048 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// /// /// If a non-null value is set before loading is started, the entry is applied when the loading is completed. + /// It is not valid to set an entry while this is loading. /// public TEntry? Entry { From 78c5ccda605a26883268359a792c0cd5013db750 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 19:18:57 +0900 Subject: [PATCH 132/173] Fix renaming a ruleset DLL causing a startup crash --- osu.Game/Rulesets/RulesetStore.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 0a34ca9598..06bb14ce17 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using Microsoft.EntityFrameworkCore.Internal; using osu.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; @@ -96,13 +97,25 @@ namespace osu.Game.Rulesets context.SaveChanges(); - // add any other modes var existingRulesets = context.RulesetInfo.ToList(); + // add any other rulesets which have assemblies present but are not yet in the database. foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) { if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) - context.RulesetInfo.Add(r.RulesetInfo); + { + var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); + + if (existingSameShortName != null) + { + // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. + // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. + // in such cases, update the instantiation info of the existing entry to point to the new one. + existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; + } + else + context.RulesetInfo.Add(r.RulesetInfo); + } } context.SaveChanges(); From 2dadc9d686071ae4e1df0e5c6859f584c28174ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 19:39:00 +0900 Subject: [PATCH 133/173] Remove unused using statement --- osu.Game/Rulesets/RulesetStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 06bb14ce17..1f12f3dfeb 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using Microsoft.EntityFrameworkCore.Internal; using osu.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; From 953683044f4440051bd39f9ff294f172aa095dd0 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 21:00:08 +0900 Subject: [PATCH 134/173] fix checkbox sounds not being used for certain checkboxes --- .../UserInterface/OsuTabControlCheckbox.cs | 22 ++++++++++++++++--- osu.Game/Overlays/Comments/CommentsHeader.cs | 17 ++++++++++++++ osu.Game/Overlays/Comments/HeaderButton.cs | 2 -- osu.Game/Overlays/OverlaySortTabControl.cs | 2 ++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index b66a4a58ce..c6121dcd17 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -4,6 +4,8 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -43,6 +45,8 @@ namespace osu.Game.Graphics.UserInterface } private const float transition_length = 500; + private Sample sampleChecked; + private Sample sampleUnchecked; public OsuTabControlCheckbox() { @@ -77,8 +81,7 @@ namespace osu.Game.Graphics.UserInterface Colour = Color4.White, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - }, - new HoverClickSounds() + } }; Current.ValueChanged += selected => @@ -91,10 +94,13 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { if (accentColour == null) AccentColour = colours.Blue; + + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); } protected override bool OnHover(HoverEvent e) @@ -111,6 +117,16 @@ namespace osu.Game.Graphics.UserInterface base.OnHoverLost(e); } + protected override void OnUserChange(bool value) + { + base.OnUserChange(value); + + if (value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); + } + private void updateFade() { box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint); diff --git a/osu.Game/Overlays/Comments/CommentsHeader.cs b/osu.Game/Overlays/Comments/CommentsHeader.cs index 0dd68bbd41..bf80655c3d 100644 --- a/osu.Game/Overlays/Comments/CommentsHeader.cs +++ b/osu.Game/Overlays/Comments/CommentsHeader.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Framework.Bindables; @@ -66,6 +68,8 @@ namespace osu.Game.Overlays.Comments public readonly BindableBool Checked = new BindableBool(); private readonly SpriteIcon checkboxIcon; + private Sample sampleChecked; + private Sample sampleUnchecked; public ShowDeletedButton() { @@ -93,6 +97,13 @@ namespace osu.Game.Overlays.Comments }); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); + } + protected override void LoadComplete() { Checked.BindValueChanged(isChecked => checkboxIcon.Icon = isChecked.NewValue ? FontAwesome.Solid.CheckSquare : FontAwesome.Regular.Square, true); @@ -102,6 +113,12 @@ namespace osu.Game.Overlays.Comments protected override bool OnClick(ClickEvent e) { Checked.Value = !Checked.Value; + + if (Checked.Value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); + return true; } } diff --git a/osu.Game/Overlays/Comments/HeaderButton.cs b/osu.Game/Overlays/Comments/HeaderButton.cs index fdc8db35ab..65172aa57c 100644 --- a/osu.Game/Overlays/Comments/HeaderButton.cs +++ b/osu.Game/Overlays/Comments/HeaderButton.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Comments { @@ -39,7 +38,6 @@ namespace osu.Game.Overlays.Comments Origin = Anchor.Centre, Margin = new MarginPadding { Horizontal = 10 } }, - new HoverClickSounds(), }); } diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index b230acca11..d4dde0db3f 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -148,6 +148,8 @@ namespace osu.Game.Overlays } } }); + + AddInternal(new HoverClickSounds()); } protected override void LoadComplete() From 6e4fc26e168b2d18b6ffbcd94e00d27011134c47 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 21:02:17 +0900 Subject: [PATCH 135/173] replace 'songselect' hover/click sounds with 'button' ones for now --- osu.Game/Graphics/UserInterface/HoverSampleSet.cs | 3 --- osu.Game/Screens/Select/FooterButton.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs index 3a8187c8f4..b88f81a143 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs @@ -16,9 +16,6 @@ namespace osu.Game.Graphics.UserInterface [Description("toolbar")] Toolbar, - [Description("songselect")] - SongSelect, - [Description("tabselect")] TabSelect, diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index afb3943a09..c3fbd767ff 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select private readonly Box light; public FooterButton() - : base(HoverSampleSet.SongSelect) + : base(HoverSampleSet.Button) { AutoSizeAxes = Axes.Both; Shear = SHEAR; From 5ce52b2669201b85c788d325f007681d41dd14d8 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 18 Jun 2021 21:14:51 +0900 Subject: [PATCH 136/173] fix ModButton duplicate click sound --- osu.Game/Overlays/Mods/ModButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 5e3733cd5e..70424101fd 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -302,7 +302,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, Font = OsuFont.GetFont(size: 18) }, - new HoverClickSounds(buttons: new[] { MouseButton.Left, MouseButton.Right }) + new HoverSounds() }; Mod = mod; From 390abccb4bf56503bda7b29934e649d0a60ee7a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 22:08:59 +0900 Subject: [PATCH 137/173] Add workaround for dropdowns playing close animation on first display --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 24d062e059..b97f12df02 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -81,9 +81,13 @@ namespace osu.Game.Graphics.UserInterface sampleClose = audio.Samples.Get(@"UI/dropdown-close"); } + // todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed. + private bool wasOpened; + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring protected override void AnimateOpen() { + wasOpened = true; this.FadeIn(300, Easing.OutQuint); sampleOpen?.Play(); } @@ -91,7 +95,8 @@ namespace osu.Game.Graphics.UserInterface protected override void AnimateClose() { this.FadeOut(300, Easing.OutQuint); - sampleClose?.Play(); + if (wasOpened) + sampleClose?.Play(); } // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring From 1f6b4b10ab93b5175ad1a0e222ca9a17a80ade30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 22:16:15 +0900 Subject: [PATCH 138/173] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 1f60f02fb1..3a1e6ba9a3 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 68ffb87c6c..fe7214de38 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 8aa79762fc..01c9b27cc7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 1ec03bf6fac74b678a543849c90062808c73b181 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Jun 2021 22:25:24 +0900 Subject: [PATCH 139/173] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 490e43b5e6..54469eec43 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8eeaad1127..560e5e2493 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index db442238ce..7b3033db9c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 69c1cd5b3430ef536c5c220bd77ed7398c4bab3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 14:17:37 +0200 Subject: [PATCH 140/173] Add failing test case for hit circle animations disable --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs new file mode 100644 index 0000000000..94ee5d7e14 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -0,0 +1,71 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Configuration; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + [TestFixture] + public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor + { + [Resolved] + private OsuConfigManager config { get; set; } + + [Test] + public void TestHitCircleAnimationDisable() + { + HitCircle hitCircle = null; + + AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0)); + toggleAnimations(true); + seekSmoothlyTo(() => hitCircle.StartTime + 10); + + AddAssert("hit circle piece has transforms", () => + { + var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); + return getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime); + }); + + AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); + toggleAnimations(false); + seekSmoothlyTo(() => hitCircle.StartTime + 10); + + AddAssert("hit circle piece has no transforms", () => + { + var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); + return getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime); + }); + } + + private HitCircle getHitCircle(int index) + => EditorBeatmap.HitObjects.OfType().ElementAt(index); + + private DrawableHitObject getDrawableObjectFor(HitObject hitObject) + => this.ChildrenOfType().Single(ho => ho.HitObject == hitObject); + + private IEnumerable getTransformsRecursively(Drawable drawable) + => drawable.ChildrenOfType().SelectMany(d => d.Transforms); + + private void toggleAnimations(bool enabled) + => AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled)); + + private void seekSmoothlyTo(Func targetTime) + { + AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke())); + AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime)); + } + } +} From e2a370f60244a0ccac1450ed604a79a2be468a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 14:57:04 +0200 Subject: [PATCH 141/173] Add coverage for hit circle fade-out duration --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 19 +++++++++++-------- .../Edit/DrawableOsuEditorRuleset.cs | 16 ++++++++-------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs index 94ee5d7e14..18de3a8414 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -13,6 +13,7 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -28,25 +29,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public void TestHitCircleAnimationDisable() { HitCircle hitCircle = null; + DrawableHitCircle drawableHitCircle = null; AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0)); toggleAnimations(true); seekSmoothlyTo(() => hitCircle.StartTime + 10); - AddAssert("hit circle piece has transforms", () => - { - var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); - return getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime); - }); + AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); + AddAssert("hit circle piece has transforms", + () => getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime)); AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); toggleAnimations(false); seekSmoothlyTo(() => hitCircle.StartTime + 10); - AddAssert("hit circle piece has no transforms", () => + AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); + AddAssert("hit circle piece has no transforms", + () => getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime)); + AddAssert("hit circle has longer fade-out applied", () => { - var drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle); - return getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime); + var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha)); + return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION; }); } diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index aeeae84d14..9143b154b9 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -20,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Edit { public class DrawableOsuEditorRuleset : DrawableOsuRuleset { + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// + public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700; + public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) { @@ -46,12 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit d.ApplyCustomUpdateState += updateState; } - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - private const double editor_hit_object_fade_out_extension = 700; - private void updateState(DrawableHitObject hitObject, ArmedState state) { if (state == ArmedState.Idle || hitAnimations.Value) @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObject is DrawableHitCircle circle) { circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension * 4) + .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4) .Expire(); circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Edit hitObject.RemoveTransform(existing); using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire(); break; } } From fe48ddfee3242cde208b64cb7a59967f0d670885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:26:31 +0200 Subject: [PATCH 142/173] Also cover slider animation disable --- .../Editor/TestSceneOsuEditorHitAnimations.cs | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs index 18de3a8414..7ffa2c1f94 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs @@ -36,16 +36,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor seekSmoothlyTo(() => hitCircle.StartTime + 10); AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); - AddAssert("hit circle piece has transforms", - () => getTransformsRecursively(drawableHitCircle.CirclePiece).Any(t => t.EndTime > EditorClock.CurrentTime)); + assertFutureTransforms(() => drawableHitCircle.CirclePiece, true); AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); toggleAnimations(false); seekSmoothlyTo(() => hitCircle.StartTime + 10); AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); - AddAssert("hit circle piece has no transforms", - () => getTransformsRecursively(drawableHitCircle.CirclePiece).All(t => t.EndTime <= EditorClock.CurrentTime)); + assertFutureTransforms(() => drawableHitCircle.CirclePiece, false); AddAssert("hit circle has longer fade-out applied", () => { var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha)); @@ -53,9 +51,47 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + [Test] + public void TestSliderAnimationDisable() + { + Slider slider = null; + DrawableSlider drawableSlider = null; + DrawableSliderRepeat sliderRepeat = null; + + AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0)); + toggleAnimations(true); + seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10); + + retrieveDrawables(); + assertFutureTransforms(() => sliderRepeat, true); + + AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1)); + toggleAnimations(false); + seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10); + + retrieveDrawables(); + assertFutureTransforms(() => sliderRepeat.Arrow, false); + seekSmoothlyTo(() => slider.GetEndTime()); + AddAssert("slider has longer fade-out applied", () => + { + var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha)); + return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION; + }); + + void retrieveDrawables() => + AddStep("retrieve drawables", () => + { + drawableSlider = (DrawableSlider)getDrawableObjectFor(slider); + sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType().First()); + }); + } + private HitCircle getHitCircle(int index) => EditorBeatmap.HitObjects.OfType().ElementAt(index); + private Slider getSliderWithRepeats(int index) + => EditorBeatmap.HitObjects.OfType().Where(s => s.RepeatCount >= 1).ElementAt(index); + private DrawableHitObject getDrawableObjectFor(HitObject hitObject) => this.ChildrenOfType().Single(ho => ho.HitObject == hitObject); @@ -70,5 +106,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke())); AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime)); } + + private void assertFutureTransforms(Func getDrawable, bool hasFutureTransforms) + => AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms", + () => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms); } } From e94fbd83e2690c5c34c8ccd0cebdacaae636f4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:30:45 +0200 Subject: [PATCH 143/173] Ensure editor ruleset animation disable execution order --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 9143b154b9..54fd95a618 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -69,8 +69,14 @@ namespace osu.Game.Rulesets.Osu.Edit if (hitObject is IHasMainCirclePiece mainPieceContainer) { // clear any explode animation logic. - mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + // this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables. + ScheduleAfterChildren(() => + { + if (hitObject.HitObject == null) return; + + mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); + mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + }); } if (hitObject is DrawableSliderRepeat repeat) From afc89b39d9cb751ab00555c50a055cb607774603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Jun 2021 15:32:51 +0200 Subject: [PATCH 144/173] Use `StateUpdateTime` for transform clearing logic `MainCirclePiece` specifies a state transform starting at `StateUpdateTime`, which is earlier than the previously-used `HitStateUpdateTime`. Change the transform clearing logic to use the former to ensure that exactly all animation transforms are cleared. --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 54fd95a618..0e61c02e2d 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -74,15 +74,15 @@ namespace osu.Game.Rulesets.Osu.Edit { if (hitObject.HitObject == null) return; - mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true); + mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true); }); } if (hitObject is DrawableSliderRepeat repeat) { - repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true); - repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true); + repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true); + repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true); } // adjust the visuals of top-level object types to make them stay on screen for longer than usual. From 843c8bd7a44e7b244214818d6e6765e997fc1bf5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Jun 2021 20:33:50 +0300 Subject: [PATCH 145/173] Move spinner approach circle to its own `SkinnableDrawable` --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- .../Objects/Drawables/DrawableSpinner.cs | 12 ++-- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 2 +- .../Skinning/Legacy/LegacySpinner.cs | 31 --------- .../Legacy/LegacySpinnerApproachCircle.cs | 64 +++++++++++++++++++ .../Legacy/OsuLegacySkinTransformer.cs | 6 ++ 6 files changed, 76 insertions(+), 41 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index c635aab5f5..d712ffd92a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Mods // todo: hide background using (spinner.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt)) - spinner.HideApproachCircle(); + spinner.ApproachCircle.Hide(); using (spinner.BeginAbsoluteSequence(fadeStartTime)) spinner.FadeOut(fadeDuration); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 942cc52c50..4507b1520c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; + public SkinnableDrawable ApproachCircle { get; private set; } + public SpinnerRotationTracker RotationTracker { get; private set; } private SpinnerSpmCalculator spmCalculator; @@ -42,8 +44,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; - internal readonly Bindable ApproachCircleVisibility = new Bindable(Visibility.Visible); - /// /// The amount of bonus score gained from spinning after the required number of spins, for display purposes. /// @@ -88,7 +88,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Y, Children = new Drawable[] { - new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()), + ApproachCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerApproachCircle)), + new SkinnableSpinnerBody(ApproachCircle.CreateProxy(), _ => new DefaultSpinner()), RotationTracker = new SpinnerRotationTracker(this) } }, @@ -287,11 +288,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables updateBonusScore(); } - /// - /// Hides the spinner's approach circle if it has one. - /// - public void HideApproachCircle() => this.TransformBindableTo(ApproachCircleVisibility, Visibility.Hidden); - private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; private int wholeSpins; diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index fcb544fa5b..687fc1f966 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Osu Cursor, CursorTrail, SliderScorePoint, - ApproachCircle, ReverseArrow, HitCircleText, SliderHeadHitCircle, @@ -19,5 +18,6 @@ namespace osu.Game.Rulesets.Osu SliderBall, SliderBody, SpinnerBody, + SpinnerApproachCircle, } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 5b0c2d405b..f32caab8ac 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -32,7 +32,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } - private Drawable approachCircle; private Sprite spin; private Sprite clear; @@ -61,7 +60,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy RelativeSizeAxes = Axes.Both, Children = new[] { - approachCircle = getSpinnerApproachCircle(source), spin = new Sprite { Anchor = Anchor.TopCentre, @@ -104,28 +102,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy }.With(s => s.Font = s.Font.With(fixedWidth: false)), } }); - - static Drawable getSpinnerApproachCircle(ISkinSource source) - { - var spinnerProvider = source.FindProvider(s => - s.GetTexture("spinner-circle") != null || - s.GetTexture("spinner-top") != null); - - if (spinnerProvider is DefaultLegacySkin) - return Empty(); - - return new Sprite - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-approachcircle"), - Scale = new Vector2(SPRITE_SCALE * 1.86f), - Y = SPINNER_Y_CENTRE, - }; - } } - private IBindable approachCircleVisibility; private IBindable gainedBonus; private IBindable spinsPerMinute; @@ -135,12 +113,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { base.LoadComplete(); - approachCircleVisibility = DrawableSpinner.ApproachCircleVisibility.GetBoundCopy(); - approachCircleVisibility.BindValueChanged(v => - { - approachCircle.Alpha = v.NewValue == Visibility.Hidden ? 0 : 1; - }, true); - gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy(); gainedBonus.BindValueChanged(bonus => { @@ -204,9 +176,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); } - using (BeginAbsoluteSequence(d.HitObject.StartTime)) - approachCircle.ScaleTo(SPRITE_SCALE * 1.86f).ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration); - double spinFadeOutLength = Math.Min(400, d.HitObject.Duration); using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true)) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs new file mode 100644 index 0000000000..92454cefa3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Skinning; +using osuTK; +using static osu.Game.Rulesets.Osu.Skinning.Legacy.LegacySpinner; + +namespace osu.Game.Rulesets.Osu.Skinning.Legacy +{ + public class LegacySpinnerApproachCircle : CompositeDrawable + { + private DrawableSpinner drawableSpinner; + + [CanBeNull] + private Sprite sprite; + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableHitObject, ISkinSource source) + { + drawableSpinner = (DrawableSpinner)drawableHitObject; + + AutoSizeAxes = Axes.Both; + + var spinnerProvider = source.FindProvider(s => + s.GetTexture("spinner-circle") != null || + s.GetTexture("spinner-top") != null); + + if (spinnerProvider is DefaultLegacySkin) + return; + + InternalChild = sprite = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-approachcircle"), + Scale = new Vector2(SPRITE_SCALE * 1.86f), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); + } + + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + { + if (!(drawableHitObject is DrawableSpinner spinner)) + return; + + using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) + sprite?.ScaleTo(SPRITE_SCALE * 0.1f, spinner.HitObject.Duration); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 3267b48ebf..a8c42a3773 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -121,6 +121,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyOldStyleSpinner(); return null; + + case OsuSkinComponents.SpinnerApproachCircle: + if (Source.GetTexture("spinner-approachcircle") != null) + return new LegacySpinnerApproachCircle(); + + return null; } } From d6b9436151a9b0a7f658f4f4c07f5230329de115 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Jun 2021 20:34:25 +0300 Subject: [PATCH 146/173] Proxy spinner approach circle before the spinner overlay components --- .../Skinning/IProxiesApproachCircle.cs | 12 +++++++ .../Skinning/Legacy/LegacySpinner.cs | 13 ++++--- .../Skinning/SkinnableSpinnerBody.cs | 34 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs create mode 100644 osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs new file mode 100644 index 0000000000..3cc0026adc --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs @@ -0,0 +1,12 @@ +// 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.Graphics.Containers; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public interface IProxiesApproachCircle + { + Container ApproachCircleTarget { get; } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index f32caab8ac..efbb27bf3f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -15,8 +15,10 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public abstract class LegacySpinner : CompositeDrawable + public abstract class LegacySpinner : CompositeDrawable, IProxiesApproachCircle { + public const float SPRITE_SCALE = 0.625f; + /// /// All constants are in osu!stable's gamefield space, which is shifted 16px downwards. /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space. @@ -26,12 +28,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f; - protected const float SPRITE_SCALE = 0.625f; - private const float spm_hide_offset = 50f; protected DrawableSpinner DrawableSpinner { get; private set; } + public Container ApproachCircleTarget { get; private set; } private Sprite spin; private Sprite clear; @@ -58,8 +59,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Depth = float.MinValue, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { + ApproachCircleTarget = new Container + { + RelativeSizeAxes = Axes.Both, + }, spin = new Sprite { Anchor = Anchor.TopCentre, diff --git a/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs b/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs new file mode 100644 index 0000000000..763b9dd677 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs @@ -0,0 +1,34 @@ +// 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.Graphics; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + /// + /// A skinnable drawable of the component, with the approach circle exposed for modification. + /// + public class SkinnableSpinnerBody : SkinnableDrawable + { + private readonly Drawable approachCircleProxy; + + public SkinnableSpinnerBody(Drawable approachCircleProxy, Func defaultImplementation = null) + : base(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), defaultImplementation) + { + this.approachCircleProxy = approachCircleProxy; + } + + protected override void SkinChanged(ISkinSource skin) + { + if (Drawable is IProxiesApproachCircle oldProxiesApproachCircle) + oldProxiesApproachCircle.ApproachCircleTarget.Remove(approachCircleProxy); + + base.SkinChanged(skin); + + if (Drawable is IProxiesApproachCircle newProxiesApproachCircle) + newProxiesApproachCircle.ApproachCircleTarget.Add(approachCircleProxy); + } + } +} From 76db87f9cb273ca92e7cf61fc7faef898d74712a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Jun 2021 08:00:36 +0200 Subject: [PATCH 147/173] Try-catch around localisation store registration Some platforms (android, older windows versions) will throw exceptions at runtime when an unsupported `CultureInfo` is attempted to be instantiated, leading to nasty crashes. Add a preventative try-catch registration to prevent the crash, and log the errors for visibility. --- osu.Game/OsuGame.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0c4d035728..7455df361c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -55,6 +55,7 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Performance; using osu.Game.Skinning.Editor; +using osu.Framework.Extensions; namespace osu.Game { @@ -585,7 +586,15 @@ namespace osu.Game foreach (var language in Enum.GetValues(typeof(Language)).OfType()) { var cultureCode = language.ToCultureCode(); - Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode)); + + try + { + Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode)); + } + catch (Exception ex) + { + Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\""); + } } // The next time this is updated is in UpdateAfterChildren, which occurs too late and results From b47774b55a4d146e4d5a24a8daeafbde2f551cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Jun 2021 08:07:37 +0200 Subject: [PATCH 148/173] Remove Tagalog language for now Rationale given in inline comment. --- osu.Game/Localisation/Language.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index 65541feedf..3c66f31c58 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.cs @@ -86,8 +86,10 @@ namespace osu.Game.Localisation [Description(@"ไทย")] th, - [Description(@"Tagalog")] - tl, + // Tagalog has no associated localisations yet, and is not supported on Xamarin platforms or Windows versions <10. + // Can be revisited if localisations ever arrive. + //[Description(@"Tagalog")] + //tl, [Description(@"Türkçe")] tr, From afcc3e14f436d0e413bf5e6dfa0cdca844f6956d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 19 Jun 2021 16:16:29 +0900 Subject: [PATCH 149/173] m --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7455df361c..d7cdb8b396 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -55,7 +55,6 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Performance; using osu.Game.Skinning.Editor; -using osu.Framework.Extensions; namespace osu.Game { From 805e6cca75ef9d6637c10497959aadabdb163a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Jun 2021 11:07:13 +0200 Subject: [PATCH 150/173] Add direct references to Realm from Xamarin projects Fixes crashes on launch due to missing `realm-wrapper` transitive dependency. --- osu.Android.props | 4 ++++ osu.iOS.props | 1 + 2 files changed, 5 insertions(+) diff --git a/osu.Android.props b/osu.Android.props index 3a1e6ba9a3..1dc99bb60a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -54,4 +54,8 @@ + + + + diff --git a/osu.iOS.props b/osu.iOS.props index 01c9b27cc7..3689ce51f2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -99,5 +99,6 @@ + From fa87aa6be51ec0558ea0a94522751c52dd3330c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Jun 2021 11:10:37 +0200 Subject: [PATCH 151/173] Add autogenerated FodyWeavers files to Xamarin projects --- osu.Android/FodyWeavers.xml | 3 +++ osu.Android/FodyWeavers.xsd | 34 ++++++++++++++++++++++++++++++++++ osu.iOS/FodyWeavers.xml | 3 +++ osu.iOS/FodyWeavers.xsd | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 osu.Android/FodyWeavers.xml create mode 100644 osu.Android/FodyWeavers.xsd create mode 100644 osu.iOS/FodyWeavers.xml create mode 100644 osu.iOS/FodyWeavers.xsd diff --git a/osu.Android/FodyWeavers.xml b/osu.Android/FodyWeavers.xml new file mode 100644 index 0000000000..cc07b89533 --- /dev/null +++ b/osu.Android/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/osu.Android/FodyWeavers.xsd b/osu.Android/FodyWeavers.xsd new file mode 100644 index 0000000000..447878c551 --- /dev/null +++ b/osu.Android/FodyWeavers.xsd @@ -0,0 +1,34 @@ + + + + + + + + + + + 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 + + + + + + + + '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 diff --git a/osu.iOS/FodyWeavers.xml b/osu.iOS/FodyWeavers.xml new file mode 100644 index 0000000000..cc07b89533 --- /dev/null +++ b/osu.iOS/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/osu.iOS/FodyWeavers.xsd b/osu.iOS/FodyWeavers.xsd new file mode 100644 index 0000000000..447878c551 --- /dev/null +++ b/osu.iOS/FodyWeavers.xsd @@ -0,0 +1,34 @@ + + + + + + + + + + + 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 + + + + + + + + '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 c04b09520da8115080bec4dfc4310e9f549493e3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Jun 2021 20:06:28 +0300 Subject: [PATCH 152/173] Replace spinner approach circle proxying logic with hooking up to `OnSkinChange` in mod --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 15 ++++++-- .../Objects/Drawables/DrawableSpinner.cs | 5 ++- .../Skinning/IHasSpinnerApproachCircle.cs | 18 ++++++++++ .../Skinning/IProxiesApproachCircle.cs | 12 ------- .../Skinning/Legacy/LegacySpinner.cs | 13 ++++--- .../Legacy/LegacySpinnerApproachCircle.cs | 9 +++-- .../Skinning/SkinnableSpinnerBody.cs | 34 ------------------- 7 files changed, 45 insertions(+), 61 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs delete mode 100644 osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs delete mode 100644 osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index d712ffd92a..071c3dc3f1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning; namespace osu.Game.Rulesets.Osu.Mods { @@ -110,8 +111,8 @@ namespace osu.Game.Rulesets.Osu.Mods // hide elements we don't care about. // todo: hide background - using (spinner.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt)) - spinner.ApproachCircle.Hide(); + spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner); + hideSpinnerApproachCircle(spinner); using (spinner.BeginAbsoluteSequence(fadeStartTime)) spinner.FadeOut(fadeDuration); @@ -163,5 +164,15 @@ namespace osu.Game.Rulesets.Osu.Mods } } } + + private static void hideSpinnerApproachCircle(DrawableSpinner spinner) + { + var approachCircle = (spinner.Body.Drawable as IHasSpinnerApproachCircle)?.ApproachCircle; + if (approachCircle == null) + return; + + using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt)) + approachCircle.Hide(); + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 4507b1520c..ec87d3bfdf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; - public SkinnableDrawable ApproachCircle { get; private set; } + public SkinnableDrawable Body { get; private set; } public SpinnerRotationTracker RotationTracker { get; private set; } @@ -88,8 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Y, Children = new Drawable[] { - ApproachCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerApproachCircle)), - new SkinnableSpinnerBody(ApproachCircle.CreateProxy(), _ => new DefaultSpinner()), + Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()), RotationTracker = new SpinnerRotationTracker(this) } }, diff --git a/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs new file mode 100644 index 0000000000..dcfc15913c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + /// + /// A common interface between implementations of the component that provide approach circles for the spinner. + /// + public interface IHasSpinnerApproachCircle + { + /// + /// The spinner approach circle. + /// + Drawable ApproachCircle { get; } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs deleted file mode 100644 index 3cc0026adc..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/IProxiesApproachCircle.cs +++ /dev/null @@ -1,12 +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.Graphics.Containers; - -namespace osu.Game.Rulesets.Osu.Skinning -{ - public interface IProxiesApproachCircle - { - Container ApproachCircleTarget { get; } - } -} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index efbb27bf3f..5471de22d4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public abstract class LegacySpinner : CompositeDrawable, IProxiesApproachCircle + public abstract class LegacySpinner : CompositeDrawable, IHasSpinnerApproachCircle { public const float SPRITE_SCALE = 0.625f; @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } - public Container ApproachCircleTarget { get; private set; } + public Drawable ApproachCircle { get; private set; } private Sprite spin; private Sprite clear; @@ -59,11 +59,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Depth = float.MinValue, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { - ApproachCircleTarget = new Container + ApproachCircle = new LegacySpinnerApproachCircle { - RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_Y_CENTRE, }, spin = new Sprite { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs index 92454cefa3..d5e510cd69 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; -using static osu.Game.Rulesets.Osu.Skinning.Legacy.LegacySpinner; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { @@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private DrawableSpinner drawableSpinner; [CanBeNull] - private Sprite sprite; + private Sprite approachCircle; [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) @@ -35,12 +34,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (spinnerProvider is DefaultLegacySkin) return; - InternalChild = sprite = new Sprite + InternalChild = approachCircle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-approachcircle"), - Scale = new Vector2(SPRITE_SCALE * 1.86f), + Scale = new Vector2(1.86f), }; } @@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return; using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) - sprite?.ScaleTo(SPRITE_SCALE * 0.1f, spinner.HitObject.Duration); + approachCircle?.ScaleTo(0.1f, spinner.HitObject.Duration); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs b/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs deleted file mode 100644 index 763b9dd677..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/SkinnableSpinnerBody.cs +++ /dev/null @@ -1,34 +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 osu.Framework.Graphics; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Osu.Skinning -{ - /// - /// A skinnable drawable of the component, with the approach circle exposed for modification. - /// - public class SkinnableSpinnerBody : SkinnableDrawable - { - private readonly Drawable approachCircleProxy; - - public SkinnableSpinnerBody(Drawable approachCircleProxy, Func defaultImplementation = null) - : base(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), defaultImplementation) - { - this.approachCircleProxy = approachCircleProxy; - } - - protected override void SkinChanged(ISkinSource skin) - { - if (Drawable is IProxiesApproachCircle oldProxiesApproachCircle) - oldProxiesApproachCircle.ApproachCircleTarget.Remove(approachCircleProxy); - - base.SkinChanged(skin); - - if (Drawable is IProxiesApproachCircle newProxiesApproachCircle) - newProxiesApproachCircle.ApproachCircleTarget.Add(approachCircleProxy); - } - } -} From c3217fd8b1f4d9169f416577b3ca07e4ada8a76c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Jun 2021 20:10:32 +0300 Subject: [PATCH 153/173] Remove leftover approach circle skin component --- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 1 - .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 6 ------ 2 files changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 687fc1f966..46e501758b 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -18,6 +18,5 @@ namespace osu.Game.Rulesets.Osu SliderBall, SliderBody, SpinnerBody, - SpinnerApproachCircle, } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index a8c42a3773..3267b48ebf 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -121,12 +121,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyOldStyleSpinner(); return null; - - case OsuSkinComponents.SpinnerApproachCircle: - if (Source.GetTexture("spinner-approachcircle") != null) - return new LegacySpinnerApproachCircle(); - - return null; } } From 46b9fd9ac8a3778201586016d577db9d775524c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Jun 2021 09:36:18 +0200 Subject: [PATCH 154/173] Remove and ignore FodyWeavers schema files --- .gitignore | 3 +++ osu.Android/FodyWeavers.xsd | 34 ---------------------------------- osu.Game/FodyWeavers.xsd | 34 ---------------------------------- osu.iOS/FodyWeavers.xsd | 34 ---------------------------------- 4 files changed, 3 insertions(+), 102 deletions(-) delete mode 100644 osu.Android/FodyWeavers.xsd delete mode 100644 osu.Game/FodyWeavers.xsd delete mode 100644 osu.iOS/FodyWeavers.xsd diff --git a/.gitignore b/.gitignore index d122d25054..de6a3ac848 100644 --- a/.gitignore +++ b/.gitignore @@ -336,3 +336,6 @@ inspectcode /BenchmarkDotNet.Artifacts *.GeneratedMSBuildEditorConfig.editorconfig + +# Fody (pulled in by Realm) - schema file +FodyWeavers.xsd diff --git a/osu.Android/FodyWeavers.xsd b/osu.Android/FodyWeavers.xsd deleted file mode 100644 index 447878c551..0000000000 --- a/osu.Android/FodyWeavers.xsd +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - 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 - - - - - - - - '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 diff --git a/osu.Game/FodyWeavers.xsd b/osu.Game/FodyWeavers.xsd deleted file mode 100644 index 447878c551..0000000000 --- a/osu.Game/FodyWeavers.xsd +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - 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 - - - - - - - - '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 diff --git a/osu.iOS/FodyWeavers.xsd b/osu.iOS/FodyWeavers.xsd deleted file mode 100644 index 447878c551..0000000000 --- a/osu.iOS/FodyWeavers.xsd +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - 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 - - - - - - - - '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 1f383532f2a7bee5caa6bc496e887c8dce0979dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Jun 2021 09:37:23 +0200 Subject: [PATCH 155/173] Move FodyWeavers configuration to solution root --- osu.Android/FodyWeavers.xml => FodyWeavers.xml | 0 osu.Game/FodyWeavers.xml | 3 --- osu.iOS/FodyWeavers.xml | 3 --- 3 files changed, 6 deletions(-) rename osu.Android/FodyWeavers.xml => FodyWeavers.xml (100%) delete mode 100644 osu.Game/FodyWeavers.xml delete mode 100644 osu.iOS/FodyWeavers.xml diff --git a/osu.Android/FodyWeavers.xml b/FodyWeavers.xml similarity index 100% rename from osu.Android/FodyWeavers.xml rename to FodyWeavers.xml diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml deleted file mode 100644 index cc07b89533..0000000000 --- a/osu.Game/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/osu.iOS/FodyWeavers.xml b/osu.iOS/FodyWeavers.xml deleted file mode 100644 index cc07b89533..0000000000 --- a/osu.iOS/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file From 32bd3107e1929cd8b9f1bb5f636ced0b8175da1f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Jun 2021 17:07:41 +0900 Subject: [PATCH 156/173] Remove high performance GC setting --- osu.Game/Performance/HighPerformanceSession.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Performance/HighPerformanceSession.cs b/osu.Game/Performance/HighPerformanceSession.cs index 96e67669c5..661c1046f1 100644 --- a/osu.Game/Performance/HighPerformanceSession.cs +++ b/osu.Game/Performance/HighPerformanceSession.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.Runtime; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -11,7 +10,6 @@ namespace osu.Game.Performance public class HighPerformanceSession : Component { private readonly IBindable localUserPlaying = new Bindable(); - private GCLatencyMode originalGCMode; [BackgroundDependencyLoader] private void load(OsuGame game) @@ -34,14 +32,10 @@ namespace osu.Game.Performance protected virtual void EnableHighPerformanceSession() { - originalGCMode = GCSettings.LatencyMode; - GCSettings.LatencyMode = GCLatencyMode.LowLatency; } protected virtual void DisableHighPerformanceSession() { - if (GCSettings.LatencyMode == GCLatencyMode.LowLatency) - GCSettings.LatencyMode = originalGCMode; } } } From 72155a7c525818431fef2c76565eb3995a1f769a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 03:37:49 +0300 Subject: [PATCH 157/173] Replace if pattern-matching check with switch cases instead --- .../Skinning/Legacy/LegacySpinnerApproachCircle.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs index d5e510cd69..d0ce8fbb47 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs @@ -53,11 +53,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSpinner spinner)) - return; + switch (drawableHitObject) + { + case DrawableSpinner spinner: + using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) + approachCircle?.ScaleTo(0.1f, spinner.HitObject.Duration); - using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) - approachCircle?.ScaleTo(0.1f, spinner.HitObject.Duration); + break; + } } } } From 01478d780da7c2ca1da8b1d74a9fc1503b3435d5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 03:43:11 +0300 Subject: [PATCH 158/173] Generalize `IHasSpinnerApproachCircle` from being spinner-specifc --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- .../{IHasSpinnerApproachCircle.cs => IHasApproachCircle.cs} | 6 +++--- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game.Rulesets.Osu/Skinning/{IHasSpinnerApproachCircle.cs => IHasApproachCircle.cs} (59%) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 071c3dc3f1..16b38cd0b1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Mods private static void hideSpinnerApproachCircle(DrawableSpinner spinner) { - var approachCircle = (spinner.Body.Drawable as IHasSpinnerApproachCircle)?.ApproachCircle; + var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle; if (approachCircle == null) return; diff --git a/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs similarity index 59% rename from osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs rename to osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs index dcfc15913c..88aa715ad9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/IHasSpinnerApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs @@ -6,12 +6,12 @@ using osu.Framework.Graphics; namespace osu.Game.Rulesets.Osu.Skinning { /// - /// A common interface between implementations of the component that provide approach circles for the spinner. + /// A common interface between skin component implementations which provide an approach circle. /// - public interface IHasSpinnerApproachCircle + public interface IHasApproachCircle { /// - /// The spinner approach circle. + /// The approach circle drawable. /// Drawable ApproachCircle { get; } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 5471de22d4..37379f4646 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public abstract class LegacySpinner : CompositeDrawable, IHasSpinnerApproachCircle + public abstract class LegacySpinner : CompositeDrawable, IHasApproachCircle { public const float SPRITE_SCALE = 0.625f; From e4705abee2bb7b2017838440cd187154e0feefbf Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 20 Jun 2021 20:07:46 -0700 Subject: [PATCH 159/173] Update custom rulesets directory link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3054f19e79..2213b42121 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ If your platform is not listed above, there is still a chance you can manually b osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates). -You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852). +You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096). ## Developing osu! From 68e28f4903937d9c2ddd821b1147df9ac9af192f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 07:35:07 +0300 Subject: [PATCH 160/173] Implement `IHasApproachCircle` in `DrawableHitCircle` as well --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 5 ++++- osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index ca2e6578db..46fc8f99b2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -19,7 +20,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece + public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece, IHasApproachCircle { public OsuAction? HitAction => HitArea.HitAction; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; @@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public HitReceptor HitArea { get; private set; } public SkinnableDrawable CirclePiece { get; private set; } + Drawable IHasApproachCircle.ApproachCircle => ApproachCircle; + private Container scaleContainer; private InputManager inputManager; diff --git a/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs index 88aa715ad9..7fbc5b144b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; namespace osu.Game.Rulesets.Osu.Skinning { /// - /// A common interface between skin component implementations which provide an approach circle. + /// A common interface between implementations which provide an approach circle. /// public interface IHasApproachCircle { From 8b2110c048d28a79244581e0d10fbf33ba1a578f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 20 Jun 2021 21:36:08 -0700 Subject: [PATCH 161/173] Add failing discussion links test --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index ecb37706b0..2c2c4dc24e 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -28,6 +28,8 @@ namespace osu.Game.Tests.Chat [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")] [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")] [TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/abc", "https://dev.ppy.sh/beatmapsets/abc")] + [TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions", "https://dev.ppy.sh/beatmapsets/discussions")] + [TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions/123", "https://dev.ppy.sh/beatmapsets/discussions/123")] public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link) { MessageFormatter.WebsiteRootUrl = "dev.ppy.sh"; From 6fda5e569ad5b4489aacd23c529570f870efd90e Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Sun, 20 Jun 2021 21:37:00 -0700 Subject: [PATCH 162/173] Fix beatmap discussion links wrongly leading to beatmap page --- osu.Game/Online/Chat/MessageFormatter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index df14d7eb1c..faee08742b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -154,6 +154,10 @@ namespace osu.Game.Online.Chat case "beatmapsets": case "d": { + if (mainArg == "discussions") + // handle discussion links externally for now + return new LinkDetails(LinkAction.External, url); + if (args.Length > 4 && int.TryParse(args[4], out var id)) // https://osu.ppy.sh/beatmapsets/1154158#osu/2768184 return new LinkDetails(LinkAction.OpenBeatmap, id.ToString()); From 42edbe4fb91741f22a076e3f7cc474ab2ddb6ef3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 08:40:38 +0300 Subject: [PATCH 163/173] Move `ApproachCircle` implementation into per-style --- .../Skinning/Legacy/LegacyNewStyleSpinner.cs | 31 ++++++++++++++----- .../Skinning/Legacy/LegacyOldStyleSpinner.cs | 12 ++++++- .../Skinning/Legacy/LegacySpinner.cs | 14 +++------ 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 22fb3aab86..09b0d83bb4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -31,12 +31,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Container scaleContainer; + public override Drawable ApproachCircle { get; protected set; } + [BackgroundDependencyLoader] private void load(ISkinSource source) { AddInternal(scaleContainer = new Container { - Scale = new Vector2(SPRITE_SCALE), Anchor = Anchor.TopCentre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, @@ -48,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-glow"), + Scale = new Vector2(SPRITE_SCALE), Blending = BlendingParameters.Additive, Colour = glowColour, }, @@ -55,28 +57,43 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-bottom") + Texture = source.GetTexture("spinner-bottom"), + Scale = new Vector2(SPRITE_SCALE), }, discTop = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-top") + Texture = source.GetTexture("spinner-top"), + Scale = new Vector2(SPRITE_SCALE), }, fixedMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-middle") + Texture = source.GetTexture("spinner-middle"), + Scale = new Vector2(SPRITE_SCALE), }, spinningMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-middle2") - } + Texture = source.GetTexture("spinner-middle2"), + Scale = new Vector2(SPRITE_SCALE), + }, } }); + + if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin)) + { + scaleContainer.Add(ApproachCircle = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-approachcircle"), + Scale = new Vector2(SPRITE_SCALE * 1.86f), + }); + } } protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) @@ -126,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy glow.Alpha = DrawableSpinner.Progress; - scaleContainer.Scale = new Vector2(SPRITE_SCALE * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); + scaleContainer.Scale = new Vector2(0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index d80e061662..8f162806de 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -29,12 +29,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private const float final_metre_height = 692 * SPRITE_SCALE; + public override Drawable ApproachCircle { get; protected set; } + [BackgroundDependencyLoader] private void load(ISkinSource source) { spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true; - AddRangeInternal(new Drawable[] + AddRangeInternal(new[] { new Sprite { @@ -68,6 +70,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.TopLeft, Scale = new Vector2(SPRITE_SCALE) } + }, + ApproachCircle = new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-approachcircle"), + Scale = new Vector2(SPRITE_SCALE * 1.86f), + Y = SPINNER_Y_CENTRE, } }); } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 37379f4646..3bbb523cfe 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } - public Drawable ApproachCircle { get; private set; } + public abstract Drawable ApproachCircle { get; protected set; } private Sprite spin; private Sprite clear; @@ -59,15 +59,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { Depth = float.MinValue, RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { - ApproachCircle = new LegacySpinnerApproachCircle - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Scale = new Vector2(SPRITE_SCALE), - Y = SPINNER_Y_CENTRE, - }, spin = new Sprite { Anchor = Anchor.TopCentre, @@ -184,6 +177,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); } + using (BeginAbsoluteSequence(d.HitObject.StartTime)) + ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration); + double spinFadeOutLength = Math.Min(400, d.HitObject.Duration); using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true)) From 036b745425441007a7aa9c377aff4697622b0295 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 08:41:10 +0300 Subject: [PATCH 164/173] Remove no longer needed `LegacySpinnerApproachCircle` --- .../Legacy/LegacySpinnerApproachCircle.cs | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs deleted file mode 100644 index d0ce8fbb47..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerApproachCircle.cs +++ /dev/null @@ -1,66 +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 JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Skinning.Legacy -{ - public class LegacySpinnerApproachCircle : CompositeDrawable - { - private DrawableSpinner drawableSpinner; - - [CanBeNull] - private Sprite approachCircle; - - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableHitObject, ISkinSource source) - { - drawableSpinner = (DrawableSpinner)drawableHitObject; - - AutoSizeAxes = Axes.Both; - - var spinnerProvider = source.FindProvider(s => - s.GetTexture("spinner-circle") != null || - s.GetTexture("spinner-top") != null); - - if (spinnerProvider is DefaultLegacySkin) - return; - - InternalChild = approachCircle = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-approachcircle"), - Scale = new Vector2(1.86f), - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); - } - - private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) - { - switch (drawableHitObject) - { - case DrawableSpinner spinner: - using (BeginAbsoluteSequence(spinner.HitObject.StartTime)) - approachCircle?.ScaleTo(0.1f, spinner.HitObject.Duration); - - break; - } - } - } -} From ba15f7c19b1c13b59a8eec859a0312efec07aba9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 10:47:38 +0300 Subject: [PATCH 165/173] Move `ApproachCircle` out of the scale container and revert relevant changes --- .../Skinning/Legacy/LegacyNewStyleSpinner.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 09b0d83bb4..82a2598443 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { AddInternal(scaleContainer = new Container { + Scale = new Vector2(SPRITE_SCALE), Anchor = Anchor.TopCentre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, @@ -49,7 +50,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-glow"), - Scale = new Vector2(SPRITE_SCALE), Blending = BlendingParameters.Additive, Colour = glowColour, }, @@ -58,40 +58,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-bottom"), - Scale = new Vector2(SPRITE_SCALE), }, discTop = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-top"), - Scale = new Vector2(SPRITE_SCALE), }, fixedMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-middle"), - Scale = new Vector2(SPRITE_SCALE), }, spinningMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-middle2"), - Scale = new Vector2(SPRITE_SCALE), }, } }); if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin)) { - scaleContainer.Add(ApproachCircle = new Sprite + AddInternal(ApproachCircle = new Sprite { - Anchor = Anchor.Centre, + Anchor = Anchor.TopCentre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-approachcircle"), Scale = new Vector2(SPRITE_SCALE * 1.86f), + Y = SPINNER_Y_CENTRE, }); } } @@ -143,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy glow.Alpha = DrawableSpinner.Progress; - scaleContainer.Scale = new Vector2(0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f); + scaleContainer.Scale = new Vector2(SPRITE_SCALE * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); } } } From 999bf27eae3540067220c61ac64c6c6c7001ab9d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Jun 2021 12:07:00 +0300 Subject: [PATCH 166/173] Remove unnecessary abstraction of `ApproachCircle` property --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs | 2 -- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs | 2 -- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 82a2598443..ae8d6a61f8 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -31,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Container scaleContainer; - public override Drawable ApproachCircle { get; protected set; } - [BackgroundDependencyLoader] private void load(ISkinSource source) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index 8f162806de..cbe721d21d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private const float final_metre_height = 692 * SPRITE_SCALE; - public override Drawable ApproachCircle { get; protected set; } - [BackgroundDependencyLoader] private void load(ISkinSource source) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 3bbb523cfe..317649785e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected DrawableSpinner DrawableSpinner { get; private set; } - public abstract Drawable ApproachCircle { get; protected set; } + public Drawable ApproachCircle { get; protected set; } private Sprite spin; private Sprite clear; From 3eb088f89a2fd3cc3b6490574a6a6963715ecc3f Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:30:04 +0200 Subject: [PATCH 167/173] Add low difficulty overlaps check --- .../Edit/Checks/CheckLowDiffOverlaps.cs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs new file mode 100644 index 0000000000..488bdfd972 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs @@ -0,0 +1,107 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckLowDiffOverlaps : ICheck + { + // For the lowest difficulties, the osu! Ranking Criteria encourages overlapping ~180 BPM 1/2, but discourages ~180 BPM 1/1. + private const double should_overlap_threshold = 150; // 200 BPM 1/2 + private const double should_probably_overlap_threshold = 175; // 170 BPM 1/2 + private const double should_not_overlap_threshold = 250; // 120 BPM 1/2 = 240 BPM 1/1 + + // Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching. + private const double overlap_leniency = 5; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Missing or unexpected overlaps"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateShouldOverlap(this), + new IssueTemplateShouldProbablyOverlap(this), + new IssueTemplateShouldNotOverlap(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + // TODO: This should also apply to *lowest difficulty* Normals - they are skipped for now. + if (context.InterpretedDifficulty > DifficultyRating.Easy) + yield break; + + var hitObjects = context.Beatmap.HitObjects; + + for (int i = 0; i < hitObjects.Count - 1; ++i) + { + if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner) + continue; + + if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner) + continue; + + var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime(); + if (deltaTime >= hitObject.TimeFadeIn + hitObject.TimePreempt) + // The objects are not visible at the same time (without mods), hence skipping. + continue; + + var distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared; + var diameter = (hitObject.Radius - overlap_leniency) * 2; + var diameterSq = diameter * diameter; + + bool areOverlapping = distanceSq < diameterSq; + + // Slider ends do not need to be overlapped because of slider leniency. + if (!areOverlapping && !(hitObject is Slider)) + { + if (deltaTime < should_overlap_threshold) + yield return new IssueTemplateShouldOverlap(this).Create(deltaTime, hitObject, nextHitObject); + else if (deltaTime < should_probably_overlap_threshold) + yield return new IssueTemplateShouldProbablyOverlap(this).Create(deltaTime, hitObject, nextHitObject); + } + + if (areOverlapping && deltaTime > should_not_overlap_threshold) + yield return new IssueTemplateShouldNotOverlap(this).Create(deltaTime, hitObject, nextHitObject); + } + } + + public abstract class IssueTemplateOverlap : IssueTemplate + { + protected IssueTemplateOverlap(ICheck check, IssueType issueType, string unformattedMessage) + : base(check, issueType, unformattedMessage) + { + } + + public Issue Create(double deltaTime, params HitObject[] hitObjects) => new Issue(hitObjects, this, deltaTime); + } + + public class IssueTemplateShouldOverlap : IssueTemplateOverlap + { + public IssueTemplateShouldOverlap(ICheck check) + : base(check, IssueType.Problem, "These are {0} ms apart and so should be overlapping.") + { + } + } + + public class IssueTemplateShouldProbablyOverlap : IssueTemplateOverlap + { + public IssueTemplateShouldProbablyOverlap(ICheck check) + : base(check, IssueType.Warning, "These are {0} ms apart and so should probably be overlapping.") + { + } + } + + public class IssueTemplateShouldNotOverlap : IssueTemplateOverlap + { + public IssueTemplateShouldNotOverlap(ICheck check) + : base(check, IssueType.Problem, "These are {0} ms apart and so should NOT be overlapping.") + { + } + } + } +} From fcb918d0e11868c7bfd3954939b2143165bf4e45 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:30:23 +0200 Subject: [PATCH 168/173] Add time distance equality check --- .../Edit/Checks/CheckTimeDistanceEquality.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs new file mode 100644 index 0000000000..db48878dd3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs @@ -0,0 +1,157 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Edit.Checks +{ + public class CheckTimeDistanceEquality : ICheck + { + private const double pattern_lifetime = 600; // Two objects this many ms apart or more are skipped. (200 BPM 2/1) + private const double stack_leniency = 12; // Two objects this distance apart or less are skipped. + + private const double observation_lifetime = 4000; // How long an observation is relevant for comparison. (120 BPM 8/1) + private const double similar_time_leniency = 16; // How different two delta times can be to still be compared. (240 BPM 1/16) + + private const double distance_leniency_absolute_warning = 10; // How many pixels are subtracted from the difference between current and expected distance. + private const double distance_leniency_percent_warning = 0.15; // How much of the current distance that the difference can make out. + private const double distance_leniency_absolute_problem = 20; + private const double distance_leniency_percent_problem = 0.3; + + public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Object too close or far away from previous"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateIrregularSpacingProblem(this), + new IssueTemplateIrregularSpacingWarning(this) + }; + + /// + /// Represents an observation of the time and distance between two objects. + /// + private readonly struct ObservedTimeDistance + { + public readonly double ObservationTime; + public readonly double DeltaTime; + public readonly double Distance; + + public ObservedTimeDistance(double observationTime, double deltaTime, double distance) + { + ObservationTime = observationTime; + DeltaTime = deltaTime; + Distance = distance; + } + } + + public IEnumerable Run(BeatmapVerifierContext context) + { + if (context.InterpretedDifficulty > DifficultyRating.Normal) + yield break; + + var prevObservedTimeDistances = new List(); + var hitObjects = context.Beatmap.HitObjects; + + for (int i = 0; i < hitObjects.Count - 1; ++i) + { + if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner) + continue; + + if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner) + continue; + + var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime(); + + // Ignore objects that are far enough apart in time to not be considered the same pattern. + if (deltaTime > pattern_lifetime) + continue; + + // Relying on FastInvSqrt is probably good enough here. We'll be taking the difference between distances later, hence square not being sufficient. + var distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast; + + // Ignore stacks and half-stacks, as these are close enough to where they can't be confused for being time-distanced. + if (distance < stack_leniency) + continue; + + var observedTimeDistance = new ObservedTimeDistance(nextHitObject.StartTime, deltaTime, distance); + var expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance); + + if (expectedDistance == 0) + { + // There was nothing relevant to compare to. + prevObservedTimeDistances.Add(observedTimeDistance); + continue; + } + + if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_problem) / distance > distance_leniency_percent_problem) + yield return new IssueTemplateIrregularSpacingProblem(this).Create(expectedDistance, distance, hitObject, nextHitObject); + else if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_warning) / distance > distance_leniency_percent_warning) + yield return new IssueTemplateIrregularSpacingWarning(this).Create(expectedDistance, distance, hitObject, nextHitObject); + else + { + // We use `else` here to prevent issues from cascading; an object spaced too far could cause regular spacing to be considered "too short" otherwise. + prevObservedTimeDistances.Add(observedTimeDistance); + } + } + } + + private double getExpectedDistance(IEnumerable prevObservedTimeDistances, ObservedTimeDistance observedTimeDistance) + { + var observations = prevObservedTimeDistances.Count(); + + int count = 0; + double sum = 0; + + // Looping this in reverse allows us to break before going through all elements, as we're only interested in the most recent ones. + for (int i = observations - 1; i >= 0; --i) + { + var prevObservedTimeDistance = prevObservedTimeDistances.ElementAt(i); + + // Only consider observations within the last few seconds - this allows the map to build spacing up/down over time, but prevents it from being too sudden. + if (observedTimeDistance.ObservationTime - prevObservedTimeDistance.ObservationTime > observation_lifetime) + break; + + // Only consider observations which have a similar time difference - this leniency allows handling of multi-BPM maps which speed up/down slowly. + if (Math.Abs(observedTimeDistance.DeltaTime - prevObservedTimeDistance.DeltaTime) > similar_time_leniency) + break; + + count += 1; + sum += prevObservedTimeDistance.Distance / Math.Max(prevObservedTimeDistance.DeltaTime, 1); + } + + return sum / Math.Max(count, 1) * observedTimeDistance.DeltaTime; + } + + public abstract class IssueTemplateIrregularSpacing : IssueTemplate + { + protected IssueTemplateIrregularSpacing(ICheck check, IssueType issueType) + : base(check, issueType, "Expected {0:0} px spacing like previous objects, currently {1:0}.") + { + } + + public Issue Create(double expected, double actual, params HitObject[] hitObjects) => new Issue(hitObjects, this, expected, actual); + } + + public class IssueTemplateIrregularSpacingProblem : IssueTemplateIrregularSpacing + { + public IssueTemplateIrregularSpacingProblem(ICheck check) + : base(check, IssueType.Problem) + { + } + } + + public class IssueTemplateIrregularSpacingWarning : IssueTemplateIrregularSpacing + { + public IssueTemplateIrregularSpacingWarning(ICheck check) + : base(check, IssueType.Warning) + { + } + } + } +} From 2f3f4f3e4b8b660b05d6652a9b1261f09901bcea Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:30:45 +0200 Subject: [PATCH 169/173] Add new checks to verifier --- osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs index 04e881fbf3..896e904f3f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs @@ -13,7 +13,12 @@ namespace osu.Game.Rulesets.Osu.Edit { private readonly List checks = new List { - new CheckOffscreenObjects() + // Compose + new CheckOffscreenObjects(), + + // Spread + new CheckTimeDistanceEquality(), + new CheckLowDiffOverlaps() }; public IEnumerable Run(BeatmapVerifierContext context) From e11139eadfe1a6d0e09e247abc02915fede9f21f Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:33:50 +0200 Subject: [PATCH 170/173] Add low difficulty overlap tests Moq is introduced to mock sliders' end time/position. This is already used similarly in `osu.Game.Tests`. --- .../Editor/Checks/CheckLowDiffOverlapsTest.cs | 260 ++++++++++++++++++ .../osu.Game.Rulesets.Osu.Tests.csproj | 1 + 2 files changed, 261 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckLowDiffOverlapsTest.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckLowDiffOverlapsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckLowDiffOverlapsTest.cs new file mode 100644 index 0000000000..fd17d11d10 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckLowDiffOverlapsTest.cs @@ -0,0 +1,260 @@ +// 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 System.Linq; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Edit.Checks; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks +{ + [TestFixture] + public class CheckLowDiffOverlapsTest + { + private CheckLowDiffOverlaps check; + + [SetUp] + public void Setup() + { + check = new CheckLowDiffOverlaps(); + } + + [Test] + public void TestNoOverlapFarApart() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(200, 0) } + } + }); + } + + [Test] + public void TestNoOverlapClose() + { + assertShouldProbablyOverlap(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 167, Position = new Vector2(200, 0) } + } + }); + } + + [Test] + public void TestNoOverlapTooClose() + { + assertShouldOverlap(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 100, Position = new Vector2(200, 0) } + } + }); + } + + [Test] + public void TestNoOverlapTooCloseExpert() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 100, Position = new Vector2(200, 0) } + } + }, DifficultyRating.Expert); + } + + [Test] + public void TestOverlapClose() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 167, Position = new Vector2(20, 0) } + } + }); + } + + [Test] + public void TestOverlapFarApart() + { + assertShouldNotOverlap(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(20, 0) } + } + }); + } + + [Test] + public void TestAlmostOverlapFarApart() + { + assertOk(new Beatmap + { + HitObjects = new List + { + // Default circle diameter is 128 px, but part of that is the fade/border of the circle. + // We want this to only be a problem when it actually looks like an overlap. + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(125, 0) } + } + }); + } + + [Test] + public void TestAlmostNotOverlapFarApart() + { + assertShouldNotOverlap(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(110, 0) } + } + }); + } + + [Test] + public void TestOverlapFarApartExpert() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(20, 0) } + } + }, DifficultyRating.Expert); + } + + [Test] + public void TestOverlapTooFarApart() + { + // Far apart enough to where the objects are not visible at the same time, and so overlapping is fine. + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 2000, Position = new Vector2(20, 0) } + } + }); + } + + [Test] + public void TestSliderTailOverlapFarApart() + { + assertShouldNotOverlap(new Beatmap + { + HitObjects = new List + { + getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object, + new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) } + } + }); + } + + [Test] + public void TestSliderTailOverlapClose() + { + assertOk(new Beatmap + { + HitObjects = new List + { + getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object, + new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) } + } + }); + } + + [Test] + public void TestSliderTailNoOverlapFarApart() + { + assertOk(new Beatmap + { + HitObjects = new List + { + getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object, + new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) } + } + }); + } + + [Test] + public void TestSliderTailNoOverlapClose() + { + // If these were circles they would need to overlap, but overlapping with slider tails is not required. + assertOk(new Beatmap + { + HitObjects = new List + { + getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object, + new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) } + } + }); + } + + private Mock getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition) + { + var mockSlider = new Mock(); + mockSlider.SetupGet(s => s.StartTime).Returns(startTime); + mockSlider.SetupGet(s => s.Position).Returns(startPosition); + mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition); + mockSlider.As().Setup(d => d.EndTime).Returns(endTime); + + return mockSlider; + } + + private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + Assert.That(check.Run(context), Is.Empty); + } + + private void assertShouldProbablyOverlap(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldProbablyOverlap)); + } + + private void assertShouldOverlap(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldOverlap)); + } + + private void assertShouldNotOverlap(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldNotOverlap)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index ebe642803b..1efd19f49d 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -3,6 +3,7 @@ + From 629c98e6a0d64e4a0dfb97ca1f876b5252e2d5dd Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 21 Jun 2021 15:34:11 +0200 Subject: [PATCH 171/173] Add time distance equality tests --- .../Checks/CheckTimeDistanceEqualityTest.cs | 324 ++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTimeDistanceEqualityTest.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTimeDistanceEqualityTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTimeDistanceEqualityTest.cs new file mode 100644 index 0000000000..49a6fd12fa --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTimeDistanceEqualityTest.cs @@ -0,0 +1,324 @@ +// 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 System.Linq; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Edit.Checks; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks +{ + [TestFixture] + public class CheckTimeDistanceEqualityTest + { + private CheckTimeDistanceEquality check; + + [SetUp] + public void Setup() + { + check = new CheckTimeDistanceEquality(); + } + + [Test] + public void TestCirclesEquidistant() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(100, 0) }, + new HitCircle { StartTime = 1500, Position = new Vector2(150, 0) } + } + }); + } + + [Test] + public void TestCirclesOneSlightlyOff() + { + assertWarning(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(80, 0) }, // Distance a quite low compared to previous. + new HitCircle { StartTime = 1500, Position = new Vector2(130, 0) } + } + }); + } + + [Test] + public void TestCirclesOneOff() + { + assertProblem(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing. + new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) } + } + }); + } + + [Test] + public void TestCirclesTwoOff() + { + assertProblem(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing. + new HitCircle { StartTime = 1500, Position = new Vector2(250, 0) } // Also twice the regular spacing. + } + }, count: 2); + } + + [Test] + public void TestCirclesStacked() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(50, 0) }, // Stacked, is fine. + new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) } + } + }); + } + + [Test] + public void TestCirclesStacking() + { + assertWarning(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(50, 0), StackHeight = 1 }, + new HitCircle { StartTime = 1500, Position = new Vector2(50, 0), StackHeight = 2 }, + new HitCircle { StartTime = 2000, Position = new Vector2(50, 0), StackHeight = 3 }, + new HitCircle { StartTime = 2500, Position = new Vector2(50, 0), StackHeight = 4 }, // Ends up far from (50; 0), causing irregular spacing. + new HitCircle { StartTime = 3000, Position = new Vector2(100, 0) } + } + }); + } + + [Test] + public void TestCirclesHalfStack() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(55, 0) }, // Basically stacked, so is fine. + new HitCircle { StartTime = 1500, Position = new Vector2(105, 0) } + } + }); + } + + [Test] + public void TestCirclesPartialOverlap() + { + assertProblem(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(65, 0) }, // Really low distance compared to previous. + new HitCircle { StartTime = 1500, Position = new Vector2(115, 0) } + } + }); + } + + [Test] + public void TestCirclesSlightlyDifferent() + { + assertOk(new Beatmap + { + HitObjects = new List + { + // Does not need to be perfect, as long as the distance is approximately correct it's sight-readable. + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(52, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(97, 0) }, + new HitCircle { StartTime = 1500, Position = new Vector2(165, 0) } + } + }); + } + + [Test] + public void TestCirclesSlowlyChanging() + { + const float multiplier = 1.2f; + + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) }, + // This gap would be a warning if it weren't for the previous pushing the average spacing up. + new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) } + } + }); + } + + [Test] + public void TestCirclesQuicklyChanging() + { + const float multiplier = 1.6f; + + var beatmap = new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) }, // Warning + new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) } // Problem + } + }; + + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(2)); + Assert.That(issues.First().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning); + Assert.That(issues.Last().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem); + } + + [Test] + public void TestCirclesTooFarApart() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 4000, Position = new Vector2(200, 0) }, // 2 seconds apart from previous, so can start from wherever. + new HitCircle { StartTime = 4500, Position = new Vector2(250, 0) } + } + }); + } + + [Test] + public void TestCirclesOneOffExpert() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Jumps are allowed in higher difficulties. + new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) } + } + }, DifficultyRating.Expert); + } + + [Test] + public void TestSpinner() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + new Spinner { StartTime = 500, EndTime = 1000 }, // Distance to and from the spinner should be ignored. If it isn't this should give a problem. + new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }, + new HitCircle { StartTime = 2000, Position = new Vector2(150, 0) } + } + }); + } + + [Test] + public void TestSliders() + { + assertOk(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object, + getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(200, 0), endPosition: new Vector2(250, 0)).Object, + new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) } + } + }); + } + + [Test] + public void TestSlidersOneOff() + { + assertProblem(new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 0, Position = new Vector2(0) }, + new HitCircle { StartTime = 500, Position = new Vector2(50, 0) }, + getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object, + getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(250, 0), endPosition: new Vector2(300, 0)).Object, // Twice the spacing. + new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) } + } + }); + } + + private Mock getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition) + { + var mockSlider = new Mock(); + mockSlider.SetupGet(s => s.StartTime).Returns(startTime); + mockSlider.SetupGet(s => s.Position).Returns(startPosition); + mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition); + mockSlider.As().Setup(d => d.EndTime).Returns(endTime); + + return mockSlider; + } + + private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating); + Assert.That(check.Run(context), Is.Empty); + } + + private void assertWarning(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning)); + } + + private void assertProblem(IBeatmap beatmap, int count = 1) + { + var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy); + var issues = check.Run(context).ToList(); + + Assert.That(issues, Has.Count.EqualTo(count)); + Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem)); + } + } +} From e9339d6100b43c72308862582215c00ff3a2950d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Jun 2021 15:16:19 +0900 Subject: [PATCH 172/173] Move some inline comments on `const`s to xmldoc instead --- .../Edit/Checks/CheckLowDiffOverlaps.cs | 4 ++- .../Edit/Checks/CheckTimeDistanceEquality.cs | 34 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs index 488bdfd972..1dd859b5b8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs @@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks private const double should_probably_overlap_threshold = 175; // 170 BPM 1/2 private const double should_not_overlap_threshold = 250; // 120 BPM 1/2 = 240 BPM 1/1 - // Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching. + /// + /// Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching. + /// private const double overlap_leniency = 5; public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Missing or unexpected overlaps"); diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs index db48878dd3..6420d9558e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs @@ -14,14 +14,36 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks { public class CheckTimeDistanceEquality : ICheck { - private const double pattern_lifetime = 600; // Two objects this many ms apart or more are skipped. (200 BPM 2/1) - private const double stack_leniency = 12; // Two objects this distance apart or less are skipped. + /// + /// Two objects this many ms apart or more are skipped. (200 BPM 2/1) + /// + private const double pattern_lifetime = 600; - private const double observation_lifetime = 4000; // How long an observation is relevant for comparison. (120 BPM 8/1) - private const double similar_time_leniency = 16; // How different two delta times can be to still be compared. (240 BPM 1/16) + /// + /// Two objects this distance apart or less are skipped. + /// + private const double stack_leniency = 12; + + /// + /// How long an observation is relevant for comparison. (120 BPM 8/1) + /// + private const double observation_lifetime = 4000; + + /// + /// How different two delta times can be to still be compared. (240 BPM 1/16) + /// + private const double similar_time_leniency = 16; + + /// + /// How many pixels are subtracted from the difference between current and expected distance. + /// + private const double distance_leniency_absolute_warning = 10; + + /// + /// How much of the current distance that the difference can make out. + /// + private const double distance_leniency_percent_warning = 0.15; - private const double distance_leniency_absolute_warning = 10; // How many pixels are subtracted from the difference between current and expected distance. - private const double distance_leniency_percent_warning = 0.15; // How much of the current distance that the difference can make out. private const double distance_leniency_absolute_problem = 20; private const double distance_leniency_percent_problem = 0.3; From b54e82eb993d2c41d0f0b98b094c8188414cdf05 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 22 Jun 2021 12:34:34 +0900 Subject: [PATCH 173/173] Remove unused argument from `CatchPlayfield` --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 3 +-- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 0e1ef90737..644facdabc 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.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.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI // only check the X position; handle all vertical space. base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y)); - public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation) + public CatchPlayfield(BeatmapDifficulty difficulty) { var droppedObjectContainer = new Container { diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 9389fa803b..8b6a074426 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); - protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation); + protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();