diff --git a/osu.Game/Configuration/RealmRulesetSetting.cs b/osu.Game/Configuration/RealmRulesetSetting.cs index 07e56ad8dd..3fea35ee9d 100644 --- a/osu.Game/Configuration/RealmRulesetSetting.cs +++ b/osu.Game/Configuration/RealmRulesetSetting.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Game.Database; using Realms; #nullable enable @@ -10,13 +8,10 @@ using Realms; namespace osu.Game.Configuration { [MapTo(@"RulesetSetting")] - public class RealmRulesetSetting : RealmObject, IHasGuidPrimaryKey + public class RealmRulesetSetting : RealmObject { - [PrimaryKey] - public Guid ID { get; set; } = Guid.NewGuid(); - [Indexed] - public int RulesetID { get; set; } + public string RulesetName { get; set; } = string.Empty; [Indexed] public int Variant { get; set; } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index e773ea9767..0e602da190 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; using Realms; @@ -31,22 +32,25 @@ namespace osu.Game.Database /// public readonly string Filename; + private readonly IDatabaseContextFactory? efContextFactory; + /// /// Version history: /// 6 ~2021-10-18 First tracked version. /// 7 2021-10-18 Changed OnlineID fields to non-nullable to add indexing support. /// 8 2021-10-29 Rebind scroll adjust keys to not have control modifier. /// 9 2021-11-04 Converted BeatmapMetadata.Author from string to RealmUser. + /// 10 2021-11-22 Use ShortName instead of RulesetID for ruleset settings. /// - private const int schema_version = 9; + private const int schema_version = 10; /// /// Lock object which is held during sections, blocking context creation during blocking periods. /// private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); - 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 refreshes = GlobalStatistics.Get(@"Realm", @"Dirty Refreshes"); + private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); private readonly object contextLock = new object(); private Realm? context; @@ -56,14 +60,14 @@ namespace osu.Game.Database get { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException($"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); + throw new InvalidOperationException(@$"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); lock (contextLock) { if (context == null) { context = CreateContext(); - Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); + 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. @@ -72,13 +76,20 @@ namespace osu.Game.Database } } - public RealmContextFactory(Storage storage, string filename) + /// + /// Construct a new instance of a realm context factory. + /// + /// The game storage which will be used to create the realm backing file. + /// The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified. + /// An EF factory used only for migration purposes. + public RealmContextFactory(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) { this.storage = storage; + this.efContextFactory = efContextFactory; Filename = filename; - const string realm_extension = ".realm"; + const string realm_extension = @".realm"; if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) Filename += realm_extension; @@ -232,10 +243,35 @@ namespace osu.Game.Database }; } + break; + + case 10: + string rulesetSettingClassName = getMappedOrOriginalName(typeof(RealmRulesetSetting)); + + var oldSettings = migration.OldRealm.DynamicApi.All(rulesetSettingClassName); + var newSettings = migration.NewRealm.All().ToList(); + + for (int i = 0; i < newSettings.Count; i++) + { + dynamic? oldItem = oldSettings.ElementAt(i); + var newItem = newSettings.ElementAt(i); + + long rulesetId = oldItem.RulesetID; + string? rulesetName = getRulesetShortNameFromLegacyID(rulesetId); + + if (string.IsNullOrEmpty(rulesetName)) + migration.NewRealm.Remove(newItem); + else + newItem.RulesetName = rulesetName; + } + break; } } + private string? getRulesetShortNameFromLegacyID(long rulesetId) => + efContextFactory?.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName; + /// /// Flush any active contexts and block any further writes. /// @@ -250,7 +286,7 @@ namespace osu.Game.Database throw new ObjectDisposedException(nameof(RealmContextFactory)); if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread."); + throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); @@ -274,7 +310,7 @@ namespace osu.Game.Database timeout -= sleep_length; if (timeout < 0) - throw new TimeoutException("Took too long to acquire lock"); + throw new TimeoutException(@"Took too long to acquire lock"); } } catch diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e207d9ce3b..1890fd7d24 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -188,7 +188,11 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); + runMigrations(); + + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); + + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", contextFactory)); dependencies.CacheAs(Storage); @@ -203,8 +207,6 @@ namespace osu.Game Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; - runMigrations(); - dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Resources, Audio)); dependencies.CacheAs(SkinManager); @@ -227,7 +229,6 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() @@ -456,7 +457,8 @@ namespace osu.Game { Key = dkb.Key, Value = dkb.StringValue, - RulesetID = dkb.RulesetID.Value, + // important: this RulesetStore must be the EF one. + RulesetName = RulesetStore.GetRuleset(dkb.RulesetID.Value).ShortName, Variant = dkb.Variant ?? 0, }); } diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index eec71a3623..17678775e9 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -20,16 +20,13 @@ namespace osu.Game.Rulesets.Configuration private List databasedSettings = new List(); - private readonly int rulesetId; + private readonly string rulesetName; protected RulesetConfigManager(SettingsStore store, RulesetInfo ruleset, int? variant = null) { realmFactory = store?.Realm; - if (realmFactory != null && !ruleset.ID.HasValue) - throw new InvalidOperationException("Attempted to add databased settings for a non-databased ruleset"); - - rulesetId = ruleset.ID ?? -1; + rulesetName = ruleset.ShortName; this.variant = variant ?? 0; @@ -43,7 +40,7 @@ namespace osu.Game.Rulesets.Configuration if (realmFactory != null) { // As long as RulesetConfigCache exists, there is no need to subscribe to realm events. - databasedSettings = realmFactory.Context.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + databasedSettings = realmFactory.Context.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).ToList(); } } @@ -68,7 +65,7 @@ namespace osu.Game.Rulesets.Configuration { foreach (var c in changed) { - var setting = realm.All().First(s => s.RulesetID == rulesetId && s.Variant == variant && s.Key == c.ToString()); + var setting = realm.All().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); setting.Value = ConfigStore[c].ToString(); } @@ -94,7 +91,7 @@ namespace osu.Game.Rulesets.Configuration { Key = lookup.ToString(), Value = bindable.Value.ToString(), - RulesetID = rulesetId, + RulesetName = rulesetName, Variant = variant, };