From cbabc4886cc7b94abca634ca618525fb32eb5cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jul 2022 17:25:11 +0200 Subject: [PATCH 1/7] Convert `ModPreset` to realm object --- osu.Game/Rulesets/Mods/ModPreset.cs | 46 +++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModPreset.cs b/osu.Game/Rulesets/Mods/ModPreset.cs index 367acc8a91..2c3f574d47 100644 --- a/osu.Game/Rulesets/Mods/ModPreset.cs +++ b/osu.Game/Rulesets/Mods/ModPreset.cs @@ -3,6 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Game.Database; +using osu.Game.Online.API; +using Realms; namespace osu.Game.Rulesets.Mods { @@ -10,12 +16,18 @@ namespace osu.Game.Rulesets.Mods /// A mod preset is a named collection of configured mods. /// Presets are presented to the user in the mod select overlay for convenience. /// - public class ModPreset + public class ModPreset : RealmObject, IHasGuidPrimaryKey, ISoftDelete { + /// + /// The internal database ID of the preset. + /// + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + /// /// The ruleset that the preset is valid for. /// - public RulesetInfo RulesetInfo { get; set; } = null!; + public RulesetInfo Ruleset { get; set; } = null!; /// /// The name of the mod preset. @@ -30,6 +42,34 @@ namespace osu.Game.Rulesets.Mods /// /// The set of configured mods that are part of the preset. /// - public ICollection Mods { get; set; } = Array.Empty(); + [Ignored] + public ICollection Mods + { + get + { + if (string.IsNullOrEmpty(ModsJson)) + return Array.Empty(); + + var apiMods = JsonConvert.DeserializeObject>(ModsJson); + var ruleset = Ruleset.CreateInstance(); + return apiMods.AsNonNull().Select(mod => mod.ToMod(ruleset)).ToArray(); + } + set + { + var apiMods = value.Select(mod => new APIMod(mod)).ToArray(); + ModsJson = JsonConvert.SerializeObject(apiMods); + } + } + + /// + /// The set of configured mods that are part of the preset, serialised as a JSON blob. + /// + [MapTo("Mods")] + public string ModsJson { get; set; } = string.Empty; + + /// + /// Whether the preset has been soft-deleted by the user. + /// + public bool DeletePending { get; set; } } } From 345f10311935da5f49e7f230acb82e3348af8fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jul 2022 18:01:11 +0200 Subject: [PATCH 2/7] Migrate mod preset column to use realm --- .../UserInterface/TestSceneModPresetColumn.cs | 61 ++++++++++++++++--- .../UserInterface/TestSceneModPresetPanel.cs | 3 +- osu.Game/Database/RealmAccess.cs | 3 +- osu.Game/Overlays/Mods/ModPresetColumn.cs | 55 +++++++++++------ osu.Game/Overlays/Mods/ModPresetPanel.cs | 11 ++-- 5 files changed, 99 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index f6209e1b42..f86072dbc0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -5,10 +5,14 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -16,29 +20,54 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneModPresetColumn : OsuTestScene { + protected override bool UseFreshStoragePerRun => true; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(Realm); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset storage", () => + { + Realm.Write(realm => + { + realm.RemoveAll(); + realm.Add(createTestPresets()); + }); + }); + } + [Test] public void TestBasicAppearance() { - ModPresetColumn modPresetColumn = null!; - + AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); AddStep("create content", () => Child = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(30), - Child = modPresetColumn = new ModPresetColumn + Child = new ModPresetColumn { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Presets = createTestPresets().ToArray() } }); - AddStep("change presets", () => modPresetColumn.Presets = createTestPresets().Skip(1).ToArray()); + AddUntilStep("3 panels visible", () => this.ChildrenOfType().Count() == 3); + + AddStep("change ruleset to mania", () => Ruleset.Value = rulesets.GetRuleset(3)); + AddUntilStep("1 panel visible", () => this.ChildrenOfType().Count() == 1); } - private static IEnumerable createTestPresets() => new[] + private IEnumerable createTestPresets() => new[] { new ModPreset { @@ -48,7 +77,8 @@ namespace osu.Game.Tests.Visual.UserInterface { new OsuModHardRock(), new OsuModDoubleTime() - } + }, + Ruleset = rulesets.GetRuleset(0).AsNonNull() }, new ModPreset { @@ -60,7 +90,8 @@ namespace osu.Game.Tests.Visual.UserInterface { ApproachRate = { Value = 0 } } - } + }, + Ruleset = rulesets.GetRuleset(0).AsNonNull() }, new ModPreset { @@ -70,7 +101,19 @@ namespace osu.Game.Tests.Visual.UserInterface { new OsuModFlashlight(), new OsuModSpinIn() - } + }, + Ruleset = rulesets.GetRuleset(0).AsNonNull() + }, + new ModPreset + { + Name = "Different ruleset", + Description = "Just to shake things up", + Mods = new Mod[] + { + new ManiaModKey4(), + new ManiaModFadeIn() + }, + Ruleset = rulesets.GetRuleset(3).AsNonNull() } }; } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index 62e63d47bc..dd3c7000a2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; @@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(0, 5), - ChildrenEnumerable = createTestPresets().Select(preset => new ModPresetPanel(preset)) + ChildrenEnumerable = createTestPresets().Select(preset => new ModPresetPanel(preset.ToLiveUnmanaged())) }); } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 1a0c03af7d..5f0ec67c71 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -66,8 +66,9 @@ namespace osu.Game.Database /// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo. /// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1. /// 21 2022-07-27 Migrate collections to realm (BeatmapCollection). + /// 22 2022-07-31 Added ModPreset. /// - private const int schema_version = 21; + private const int schema_version = 22; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. diff --git a/osu.Game/Overlays/Mods/ModPresetColumn.cs b/osu.Game/Overlays/Mods/ModPresetColumn.cs index 1eea8383f8..bed4cff0ea 100644 --- a/osu.Game/Overlays/Mods/ModPresetColumn.cs +++ b/osu.Game/Overlays/Mods/ModPresetColumn.cs @@ -7,31 +7,24 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Localisation; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osuTK; +using Realms; namespace osu.Game.Overlays.Mods { public class ModPresetColumn : ModSelectColumn { - private IReadOnlyList presets = Array.Empty(); + [Resolved] + private RealmAccess realm { get; set; } = null!; - /// - /// Sets the collection of available mod presets. - /// - public IReadOnlyList Presets - { - get => presets; - set - { - presets = value; - - if (IsLoaded) - asyncLoadPanels(); - } - } + [Resolved] + private IBindable ruleset { get; set; } = null!; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -44,7 +37,20 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - asyncLoadPanels(); + ruleset.BindValueChanged(_ => rulesetChanged(), true); + } + + private IDisposable? presetSubscription; + + private void rulesetChanged() + { + presetSubscription?.Dispose(); + presetSubscription = realm.RegisterForNotifications(r => + r.All() + .Filter($"{nameof(ModPreset.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0" + + $" && {nameof(ModPreset.DeletePending)} == false", ruleset.Value.ShortName) + .OrderBy(preset => preset.Name), + (presets, _, _) => asyncLoadPanels(presets)); } private CancellationTokenSource? cancellationTokenSource; @@ -52,11 +58,17 @@ namespace osu.Game.Overlays.Mods private Task? latestLoadTask; internal bool ItemsLoaded => latestLoadTask == null; - private void asyncLoadPanels() + private void asyncLoadPanels(IReadOnlyList presets) { cancellationTokenSource?.Cancel(); - var panels = presets.Select(preset => new ModPresetPanel(preset) + if (!presets.Any()) + { + ItemsFlow.Clear(); + return; + } + + var panels = presets.Select(preset => new ModPresetPanel(preset.ToLive(realm)) { Shear = Vector2.Zero }); @@ -73,5 +85,12 @@ namespace osu.Game.Overlays.Mods latestLoadTask = null; }); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + presetSubscription?.Dispose(); + } } } diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 47e2f25538..a00729d9fd 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; @@ -11,16 +12,16 @@ namespace osu.Game.Overlays.Mods { public class ModPresetPanel : ModSelectPanel, IHasCustomTooltip { - public readonly ModPreset Preset; + public readonly Live Preset; public override BindableBool Active { get; } = new BindableBool(); - public ModPresetPanel(ModPreset preset) + public ModPresetPanel(Live preset) { Preset = preset; - Title = preset.Name; - Description = preset.Description; + Title = preset.Value.Name; + Description = preset.Value.Description; } [BackgroundDependencyLoader] @@ -29,7 +30,7 @@ namespace osu.Game.Overlays.Mods AccentColour = colours.Orange1; } - public ModPreset TooltipContent => Preset; + public ModPreset TooltipContent => Preset.Value; public ITooltip GetCustomTooltip() => new ModPresetTooltip(ColourProvider); } } From c837848238f3b75b9a567b412916f4117b656571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jul 2022 18:09:27 +0200 Subject: [PATCH 3/7] Add extended test coverage of preset realm subscription --- .../UserInterface/TestSceneModPresetColumn.cs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index f86072dbc0..f927d83722 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestBasicAppearance() + public void TestBasicOperation() { AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); AddStep("create content", () => Child = new Container @@ -65,6 +65,41 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change ruleset to mania", () => Ruleset.Value = rulesets.GetRuleset(3)); AddUntilStep("1 panel visible", () => this.ChildrenOfType().Count() == 1); + + AddStep("add another mania preset", () => Realm.Write(r => r.Add(new ModPreset + { + Name = "and another one", + Mods = new Mod[] + { + new ManiaModMirror(), + new ManiaModNightcore(), + new ManiaModHardRock() + }, + Ruleset = rulesets.GetRuleset(3).AsNonNull() + }))); + AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); + + AddStep("add another osu! preset", () => Realm.Write(r => r.Add(new ModPreset + { + Name = "hdhr", + Mods = new Mod[] + { + new OsuModHidden(), + new OsuModHardRock() + }, + Ruleset = rulesets.GetRuleset(0).AsNonNull() + }))); + AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); + + AddStep("remove mania preset", () => Realm.Write(r => + { + var toRemove = r.All().Single(preset => preset.Name == "Different ruleset"); + r.Remove(toRemove); + })); + AddUntilStep("1 panel visible", () => this.ChildrenOfType().Count() == 1); + + AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); + AddUntilStep("4 panels visible", () => this.ChildrenOfType().Count() == 4); } private IEnumerable createTestPresets() => new[] From 9dea8e3d12ef84ca23b4d36a2e114dc5e3501218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jul 2022 18:12:55 +0200 Subject: [PATCH 4/7] Add test coverage of preset soft deletion --- .../UserInterface/TestSceneModPresetColumn.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index f927d83722..2ef6d2ab70 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -102,6 +102,44 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("4 panels visible", () => this.ChildrenOfType().Count() == 4); } + [Test] + public void TestSoftDeleteSupport() + { + AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + AddUntilStep("3 panels visible", () => this.ChildrenOfType().Count() == 3); + + AddStep("soft delete preset", () => Realm.Write(r => + { + var toSoftDelete = r.All().Single(preset => preset.Name == "AR0"); + toSoftDelete.DeletePending = true; + })); + AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); + + AddStep("soft delete all presets", () => Realm.Write(r => + { + foreach (var preset in r.All()) + preset.DeletePending = true; + })); + AddUntilStep("no panels visible", () => this.ChildrenOfType().Count() == 0); + + AddStep("undelete preset", () => Realm.Write(r => + { + foreach (var preset in r.All()) + preset.DeletePending = false; + })); + AddUntilStep("3 panels visible", () => this.ChildrenOfType().Count() == 3); + } + private IEnumerable createTestPresets() => new[] { new ModPreset From 9d3cdae4bba5cd15eaaecea1b9ad32107541103c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jul 2022 21:56:10 +0200 Subject: [PATCH 5/7] Fix test scene to handle restarts properly --- .../UserInterface/TestSceneModPresetColumn.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 2ef6d2ab70..593c8abac4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -22,8 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface { protected override bool UseFreshStoragePerRun => true; - [Resolved] - private RulesetStore rulesets { get; set; } = null!; + private RulesetStore rulesets = null!; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); @@ -31,18 +30,25 @@ namespace osu.Game.Tests.Visual.UserInterface [BackgroundDependencyLoader] private void load() { + Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(Realm); } [SetUpSteps] public void SetUpSteps() { + AddStep("clear contents", Clear); AddStep("reset storage", () => { Realm.Write(realm => { realm.RemoveAll(); - realm.Add(createTestPresets()); + + var testPresets = createTestPresets(); + foreach (var preset in testPresets) + preset.Ruleset = realm.Find(preset.Ruleset.ShortName); + + realm.Add(testPresets); }); }); } @@ -75,7 +81,7 @@ namespace osu.Game.Tests.Visual.UserInterface new ManiaModNightcore(), new ManiaModHardRock() }, - Ruleset = rulesets.GetRuleset(3).AsNonNull() + Ruleset = r.Find("mania") }))); AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); @@ -87,7 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface new OsuModHidden(), new OsuModHardRock() }, - Ruleset = rulesets.GetRuleset(0).AsNonNull() + Ruleset = r.Find("osu") }))); AddUntilStep("2 panels visible", () => this.ChildrenOfType().Count() == 2); @@ -140,7 +146,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("3 panels visible", () => this.ChildrenOfType().Count() == 3); } - private IEnumerable createTestPresets() => new[] + private ICollection createTestPresets() => new[] { new ModPreset { From 5a34122a85c746e5eb1d954c6df898519ef9ef2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jul 2022 21:29:30 +0200 Subject: [PATCH 6/7] Fix test breakage after realm migration --- .../Visual/UserInterface/TestSceneModPresetPanel.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index dd3c7000a2..92d1cba2c2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -11,6 +11,7 @@ using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osuTK; @@ -46,7 +47,8 @@ namespace osu.Game.Tests.Visual.UserInterface { new OsuModHardRock(), new OsuModDoubleTime() - } + }, + Ruleset = new OsuRuleset().RulesetInfo }, new ModPreset { @@ -58,7 +60,8 @@ namespace osu.Game.Tests.Visual.UserInterface { ApproachRate = { Value = 0 } } - } + }, + Ruleset = new OsuRuleset().RulesetInfo }, new ModPreset { @@ -68,7 +71,8 @@ namespace osu.Game.Tests.Visual.UserInterface { new OsuModFlashlight(), new OsuModSpinIn() - } + }, + Ruleset = new OsuRuleset().RulesetInfo } }; } From 85f77abee113342d224c03353f9aa2a7e597e1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jul 2022 22:59:33 +0200 Subject: [PATCH 7/7] Fix code quality inspection about ambiguous equality --- osu.Game/Overlays/Mods/ModPresetTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 68da649e81..97d118fbfd 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Mods public void SetContent(ModPreset preset) { - if (preset == lastPreset) + if (ReferenceEquals(preset, lastPreset)) return; lastPreset = preset;