mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 16:32:54 +08:00
Merge pull request #19510 from bdach/mod-overlay/presets-persistence
Add persistence of mod presets to realm
This commit is contained in:
commit
8ee4f1e60b
@ -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,133 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public class TestSceneModPresetColumn : OsuTestScene
|
||||
{
|
||||
protected override bool UseFreshStoragePerRun => true;
|
||||
|
||||
private RulesetStore rulesets = null!;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[Test]
|
||||
public void TestBasicAppearance()
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
ModPresetColumn modPresetColumn = null!;
|
||||
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<ModPreset>();
|
||||
|
||||
var testPresets = createTestPresets();
|
||||
foreach (var preset in testPresets)
|
||||
preset.Ruleset = realm.Find<RulesetInfo>(preset.Ruleset.ShortName);
|
||||
|
||||
realm.Add(testPresets);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicOperation()
|
||||
{
|
||||
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<ModPresetPanel>().Count() == 3);
|
||||
|
||||
AddStep("change ruleset to mania", () => Ruleset.Value = rulesets.GetRuleset(3));
|
||||
AddUntilStep("1 panel visible", () => this.ChildrenOfType<ModPresetPanel>().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 = r.Find<RulesetInfo>("mania")
|
||||
})));
|
||||
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
|
||||
|
||||
AddStep("add another osu! preset", () => Realm.Write(r => r.Add(new ModPreset
|
||||
{
|
||||
Name = "hdhr",
|
||||
Mods = new Mod[]
|
||||
{
|
||||
new OsuModHidden(),
|
||||
new OsuModHardRock()
|
||||
},
|
||||
Ruleset = r.Find<RulesetInfo>("osu")
|
||||
})));
|
||||
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
|
||||
|
||||
AddStep("remove mania preset", () => Realm.Write(r =>
|
||||
{
|
||||
var toRemove = r.All<ModPreset>().Single(preset => preset.Name == "Different ruleset");
|
||||
r.Remove(toRemove);
|
||||
}));
|
||||
AddUntilStep("1 panel visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 1);
|
||||
|
||||
AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
|
||||
AddUntilStep("4 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 4);
|
||||
}
|
||||
|
||||
private static IEnumerable<ModPreset> createTestPresets() => new[]
|
||||
[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<ModPresetPanel>().Count() == 3);
|
||||
|
||||
AddStep("soft delete preset", () => Realm.Write(r =>
|
||||
{
|
||||
var toSoftDelete = r.All<ModPreset>().Single(preset => preset.Name == "AR0");
|
||||
toSoftDelete.DeletePending = true;
|
||||
}));
|
||||
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
|
||||
|
||||
AddStep("soft delete all presets", () => Realm.Write(r =>
|
||||
{
|
||||
foreach (var preset in r.All<ModPreset>())
|
||||
preset.DeletePending = true;
|
||||
}));
|
||||
AddUntilStep("no panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 0);
|
||||
|
||||
AddStep("undelete preset", () => Realm.Write(r =>
|
||||
{
|
||||
foreach (var preset in r.All<ModPreset>())
|
||||
preset.DeletePending = false;
|
||||
}));
|
||||
AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3);
|
||||
}
|
||||
|
||||
private ICollection<ModPreset> createTestPresets() => new[]
|
||||
{
|
||||
new ModPreset
|
||||
{
|
||||
@ -48,7 +156,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
new OsuModHardRock(),
|
||||
new OsuModDoubleTime()
|
||||
}
|
||||
},
|
||||
Ruleset = rulesets.GetRuleset(0).AsNonNull()
|
||||
},
|
||||
new ModPreset
|
||||
{
|
||||
@ -60,7 +169,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
ApproachRate = { Value = 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
Ruleset = rulesets.GetRuleset(0).AsNonNull()
|
||||
},
|
||||
new ModPreset
|
||||
{
|
||||
@ -70,7 +180,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()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -7,9 +7,11 @@ 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;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osuTK;
|
||||
|
||||
@ -31,7 +33,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()))
|
||||
});
|
||||
}
|
||||
|
||||
@ -45,7 +47,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
new OsuModHardRock(),
|
||||
new OsuModDoubleTime()
|
||||
}
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
new ModPreset
|
||||
{
|
||||
@ -57,7 +60,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
ApproachRate = { Value = 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
new ModPreset
|
||||
{
|
||||
@ -67,7 +71,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
new OsuModFlashlight(),
|
||||
new OsuModSpinIn()
|
||||
}
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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.
|
||||
/// </summary>
|
||||
private const int schema_version = 21;
|
||||
private const int schema_version = 22;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
|
@ -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<ModPreset> presets = Array.Empty<ModPreset>();
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the collection of available mod presets.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ModPreset> Presets
|
||||
{
|
||||
get => presets;
|
||||
set
|
||||
{
|
||||
presets = value;
|
||||
|
||||
if (IsLoaded)
|
||||
asyncLoadPanels();
|
||||
}
|
||||
}
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> 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<ModPreset>()
|
||||
.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<ModPreset> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<ModPreset>
|
||||
{
|
||||
public readonly ModPreset Preset;
|
||||
public readonly Live<ModPreset> Preset;
|
||||
|
||||
public override BindableBool Active { get; } = new BindableBool();
|
||||
|
||||
public ModPresetPanel(ModPreset preset)
|
||||
public ModPresetPanel(Live<ModPreset> 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<ModPreset> GetCustomTooltip() => new ModPresetTooltip(ColourProvider);
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
public void SetContent(ModPreset preset)
|
||||
{
|
||||
if (preset == lastPreset)
|
||||
if (ReferenceEquals(preset, lastPreset))
|
||||
return;
|
||||
|
||||
lastPreset = preset;
|
||||
|
@ -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.
|
||||
/// </summary>
|
||||
public class ModPreset
|
||||
public class ModPreset : RealmObject, IHasGuidPrimaryKey, ISoftDelete
|
||||
{
|
||||
/// <summary>
|
||||
/// The internal database ID of the preset.
|
||||
/// </summary>
|
||||
[PrimaryKey]
|
||||
public Guid ID { get; set; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// The ruleset that the preset is valid for.
|
||||
/// </summary>
|
||||
public RulesetInfo RulesetInfo { get; set; } = null!;
|
||||
public RulesetInfo Ruleset { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the mod preset.
|
||||
@ -30,6 +42,34 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// <summary>
|
||||
/// The set of configured mods that are part of the preset.
|
||||
/// </summary>
|
||||
public ICollection<Mod> Mods { get; set; } = Array.Empty<Mod>();
|
||||
[Ignored]
|
||||
public ICollection<Mod> Mods
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(ModsJson))
|
||||
return Array.Empty<Mod>();
|
||||
|
||||
var apiMods = JsonConvert.DeserializeObject<IEnumerable<APIMod>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The set of configured mods that are part of the preset, serialised as a JSON blob.
|
||||
/// </summary>
|
||||
[MapTo("Mods")]
|
||||
public string ModsJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the preset has been soft-deleted by the user.
|
||||
/// </summary>
|
||||
public bool DeletePending { get; set; }
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user