diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs index 4f810ce17f..03ee7c9204 100644 --- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index fd6bd9b714..55c0cf6a3b 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs index 65cfb2bff4..b45505678c 100644 --- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs index fd6bd9b714..55c0cf6a3b 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/VisualTestRunner.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/osu.Android.props b/osu.Android.props index b296c114e9..4e5b9fdbb1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 7ec7d53a7e..b944068e78 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -55,7 +55,7 @@ namespace osu.Desktop } } - using (DesktopGameHost host = Host.GetSuitableHost(gameName, true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true })) { host.ExceptionThrown += handleException; diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs index bb22fab51c..bf9467700c 100644 --- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs +++ b/osu.Game.Benchmarks/BenchmarkRealmReads.cs @@ -16,7 +16,7 @@ namespace osu.Game.Benchmarks public class BenchmarkRealmReads : BenchmarkTest { private TemporaryNativeStorage storage; - private RealmContextFactory realmFactory; + private RealmAccess realm; private UpdateThread updateThread; [Params(1, 100, 1000)] @@ -27,9 +27,9 @@ namespace osu.Game.Benchmarks storage = new TemporaryNativeStorage("realm-benchmark"); storage.DeleteDirectory(string.Empty); - realmFactory = new RealmContextFactory(storage, "client"); + realm = new RealmAccess(storage, "client"); - realmFactory.Run(realm => + realm.Run(r => { realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); }); @@ -41,9 +41,9 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkDirectPropertyRead() { - realmFactory.Run(realm => + realm.Run(r => { - var beatmapSet = realm.All().First(); + var beatmapSet = r.All().First(); for (int i = 0; i < ReadsPerFetch; i++) { @@ -61,7 +61,7 @@ namespace osu.Game.Benchmarks { try { - var beatmapSet = realmFactory.Context.All().First(); + var beatmapSet = realm.Realm.All().First(); for (int i = 0; i < ReadsPerFetch; i++) { @@ -80,9 +80,9 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkRealmLivePropertyRead() { - realmFactory.Run(realm => + realm.Run(r => { - var beatmapSet = realm.All().First().ToLive(realmFactory); + var beatmapSet = r.All().First().ToLive(realm); for (int i = 0; i < ReadsPerFetch; i++) { @@ -100,7 +100,7 @@ namespace osu.Game.Benchmarks { try { - var beatmapSet = realmFactory.Context.All().First().ToLive(realmFactory); + var beatmapSet = realm.Realm.All().First().ToLive(realm); for (int i = 0; i < ReadsPerFetch; i++) { @@ -119,9 +119,9 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkDetachedPropertyRead() { - realmFactory.Run(realm => + realm.Run(r => { - var beatmapSet = realm.All().First().Detach(); + var beatmapSet = r.All().First().Detach(); for (int i = 0; i < ReadsPerFetch; i++) { @@ -133,7 +133,7 @@ namespace osu.Game.Benchmarks [GlobalCleanup] public void Cleanup() { - realmFactory?.Dispose(); + realm?.Dispose(); storage?.Dispose(); updateThread?.Exit(); } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs index f399f48ebd..2d92c925d7 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; @@ -15,9 +16,26 @@ namespace osu.Game.Rulesets.Catch.Mods { public override double ScoreMultiplier => 1.12; - private const float default_flashlight_size = 350; + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public override BindableNumber SizeMultiplier { get; } = new BindableNumber + { + MinValue = 0.5f, + MaxValue = 1.5f, + Default = 1f, + Value = 1f, + Precision = 0.1f + }; - public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield); + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public override BindableBool ComboBasedSize { get; } = new BindableBool + { + Default = true, + Value = true + }; + + public override float DefaultFlashlightSize => 350; + + protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield); private CatchPlayfield playfield; @@ -31,10 +49,11 @@ namespace osu.Game.Rulesets.Catch.Mods { private readonly CatchPlayfield playfield; - public CatchFlashlight(CatchPlayfield playfield) + public CatchFlashlight(CatchModFlashlight modFlashlight, CatchPlayfield playfield) + : base(modFlashlight) { this.playfield = playfield; - FlashlightSize = new Vector2(0, getSizeFor(0)); + FlashlightSize = new Vector2(0, GetSizeFor(0)); } protected override void Update() @@ -44,19 +63,9 @@ namespace osu.Game.Rulesets.Catch.Mods FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this); } - private float getSizeFor(int combo) - { - if (combo > 200) - return default_flashlight_size * 0.8f; - else if (combo > 100) - return default_flashlight_size * 0.9f; - else - return default_flashlight_size; - } - protected override void OnComboChange(ValueChangedEvent e) { - this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); + this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "CircularFlashlight"; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 86a00271e9..1ee4ea12e3 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Layout; +using osu.Game.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; using osuTK; @@ -16,17 +17,35 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModHidden) }; - private const float default_flashlight_size = 180; + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public override BindableNumber SizeMultiplier { get; } = new BindableNumber + { + MinValue = 0.5f, + MaxValue = 3f, + Default = 1f, + Value = 1f, + Precision = 0.1f + }; - public override Flashlight CreateFlashlight() => new ManiaFlashlight(); + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public override BindableBool ComboBasedSize { get; } = new BindableBool + { + Default = false, + Value = false + }; + + public override float DefaultFlashlightSize => 50; + + protected override Flashlight CreateFlashlight() => new ManiaFlashlight(this); private class ManiaFlashlight : Flashlight { private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); - public ManiaFlashlight() + public ManiaFlashlight(ManiaModFlashlight modFlashlight) + : base(modFlashlight) { - FlashlightSize = new Vector2(0, default_flashlight_size); + FlashlightSize = new Vector2(DrawWidth, GetSizeFor(0)); AddLayout(flashlightProperties); } @@ -46,6 +65,7 @@ namespace osu.Game.Rulesets.Mania.Mods protected override void OnComboChange(ValueChangedEvent e) { + this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "RectangularFlashlight"; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs new file mode 100644 index 0000000000..4750c97566 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; +using osu.Game.Screens.Edit.Timing; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneSliderVelocityAdjust : OsuGameTestScene + { + private Screens.Edit.Editor editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor; + + private EditorBeatmap editorBeatmap => editor.ChildrenOfType().FirstOrDefault(); + + private EditorClock editorClock => editor.ChildrenOfType().FirstOrDefault(); + + private Slider slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); + + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(); + + private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); + + private IndeterminateSliderWithTextBoxInput velocityTextBox => Game.ChildrenOfType().First().ChildrenOfType>().First(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + private bool editorComponentsReady => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && editor?.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; + + [TestCase(true)] + [TestCase(false)] + public void TestVelocityChangeSavesCorrectly(bool adjustVelocity) + { + double? velocity = null; + + AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("wait for editor load", () => editorComponentsReady); + + AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); + AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); + + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre)); + AddStep("start placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); + AddStep("end placement", () => InputManager.Click(MouseButton.Right)); + + AddStep("exit placement mode", () => InputManager.Key(Key.Number1)); + + AddAssert("slider placed", () => slider != null); + + AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider)); + + AddAssert("ensure one slider placed", () => slider != null); + + AddStep("store velocity", () => velocity = slider.Velocity); + + if (adjustVelocity) + { + AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); + AddStep("change velocity", () => velocityTextBox.Current.Value = 2); + + AddAssert("velocity adjusted", () => + { + Debug.Assert(velocity != null); + return Precision.AlmostEquals(velocity.Value * 2, slider.Velocity); + }); + + AddStep("store velocity", () => velocity = slider.Velocity); + } + + AddStep("save", () => InputManager.Keys(PlatformAction.Save)); + AddStep("exit", () => InputManager.Key(Key.Escape)); + + AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("wait for editor load", () => editorComponentsReady); + + AddStep("seek to slider", () => editorClock.Seek(slider.StartTime)); + AddAssert("slider has correct velocity", () => slider.Velocity == velocity); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 300a9d48aa..b4eff57c55 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.UI; using osuTK; namespace osu.Game.Rulesets.Osu.Mods @@ -21,27 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods { public override double ScoreMultiplier => 1.12; - private const float default_flashlight_size = 180; - private const double default_follow_delay = 120; - private OsuFlashlight flashlight; - - public override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(); - - public void ApplyToDrawableHitObject(DrawableHitObject drawable) - { - if (drawable is DrawableSlider s) - s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; - } - - public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - base.ApplyToDrawableRuleset(drawableRuleset); - - flashlight.FollowDelay = FollowDelay.Value; - } - [SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")] public BindableNumber FollowDelay { get; } = new BindableDouble(default_follow_delay) { @@ -50,13 +30,45 @@ namespace osu.Game.Rulesets.Osu.Mods Precision = default_follow_delay, }; + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public override BindableNumber SizeMultiplier { get; } = new BindableNumber + { + MinValue = 0.5f, + MaxValue = 2f, + Default = 1f, + Value = 1f, + Precision = 0.1f + }; + + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public override BindableBool ComboBasedSize { get; } = new BindableBool + { + Default = true, + Value = true + }; + + public override float DefaultFlashlightSize => 180; + + private OsuFlashlight flashlight; + + protected override Flashlight CreateFlashlight() => flashlight = new OsuFlashlight(this); + + public void ApplyToDrawableHitObject(DrawableHitObject drawable) + { + if (drawable is DrawableSlider s) + s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; + } + private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition { - public double FollowDelay { private get; set; } + private readonly double followDelay; - public OsuFlashlight() + public OsuFlashlight(OsuModFlashlight modFlashlight) + : base(modFlashlight) { - FlashlightSize = new Vector2(0, getSizeFor(0)); + followDelay = modFlashlight.FollowDelay.Value; + + FlashlightSize = new Vector2(0, GetSizeFor(0)); } public void OnSliderTrackingChange(ValueChangedEvent e) @@ -71,24 +83,14 @@ namespace osu.Game.Rulesets.Osu.Mods var destination = e.MousePosition; FlashlightPosition = Interpolation.ValueAt( - Math.Min(Math.Abs(Clock.ElapsedFrameTime), FollowDelay), position, destination, 0, FollowDelay, Easing.Out); + Math.Min(Math.Abs(Clock.ElapsedFrameTime), followDelay), position, destination, 0, followDelay, Easing.Out); return base.OnMouseMove(e); } - private float getSizeFor(int combo) - { - if (combo > 200) - return default_flashlight_size * 0.8f; - else if (combo > 100) - return default_flashlight_size * 0.9f; - else - return default_flashlight_size; - } - protected override void OnComboChange(ValueChangedEvent e) { - this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); + this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); } protected override string FragmentShader => "CircularFlashlight"; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index 0a325f174e..fb07c687bb 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -4,6 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Layout; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; @@ -16,9 +17,26 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override double ScoreMultiplier => 1.12; - private const float default_flashlight_size = 250; + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public override BindableNumber SizeMultiplier { get; } = new BindableNumber + { + MinValue = 0.5f, + MaxValue = 1.5f, + Default = 1f, + Value = 1f, + Precision = 0.1f + }; - public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield); + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public override BindableBool ComboBasedSize { get; } = new BindableBool + { + Default = true, + Value = true + }; + + public override float DefaultFlashlightSize => 250; + + protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, playfield); private TaikoPlayfield playfield; @@ -33,7 +51,8 @@ namespace osu.Game.Rulesets.Taiko.Mods private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); private readonly TaikoPlayfield taikoPlayfield; - public TaikoFlashlight(TaikoPlayfield taikoPlayfield) + public TaikoFlashlight(TaikoModFlashlight modFlashlight, TaikoPlayfield taikoPlayfield) + : base(modFlashlight) { this.taikoPlayfield = taikoPlayfield; FlashlightSize = getSizeFor(0); @@ -43,15 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods private Vector2 getSizeFor(int combo) { - float size = default_flashlight_size; - - if (combo > 200) - size *= 0.8f; - else if (combo > 100) - size *= 0.9f; - // Preserve flashlight size through the playfield's aspect adjustment. - return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); + return new Vector2(0, GetSizeFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); } protected override void OnComboChange(ValueChangedEvent e) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index 8ca996159b..a106c4f629 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default if (!effectPoint.KiaiMode) return; - if (beatIndex % (int)timingPoint.TimeSignature != 0) + if (beatIndex % timingPoint.TimeSignature.Numerator != 0) return; double duration = timingPoint.BeatLength * 2; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 6ec14e6351..0459753b28 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -178,17 +178,17 @@ namespace osu.Game.Tests.Beatmaps.Formats var timingPoint = controlPoints.TimingPointAt(0); Assert.AreEqual(956, timingPoint.Time); Assert.AreEqual(329.67032967033, timingPoint.BeatLength); - Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); + Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); timingPoint = controlPoints.TimingPointAt(48428); Assert.AreEqual(956, timingPoint.Time); Assert.AreEqual(329.67032967033d, timingPoint.BeatLength); - Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); + Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); timingPoint = controlPoints.TimingPointAt(119637); Assert.AreEqual(119637, timingPoint.Time); Assert.AreEqual(659.340659340659, timingPoint.BeatLength); - Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); + Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); var difficultyPoint = controlPoints.DifficultyPointAt(0); Assert.AreEqual(0, difficultyPoint.Time); diff --git a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs index 7aa2dc7093..9e440c6bce 100644 --- a/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs +++ b/osu.Game.Tests/Beatmaps/IO/BeatmapImportHelper.cs @@ -53,9 +53,9 @@ namespace osu.Game.Tests.Beatmaps.IO private static void ensureLoaded(OsuGameBase osu, int timeout = 60000) { - var realmContextFactory = osu.Dependencies.Get(); + var realm = osu.Dependencies.Get(); - realmContextFactory.Run(realm => BeatmapImporterTests.EnsureLoaded(realm, timeout)); + realm.Run(r => BeatmapImporterTests.EnsureLoaded(r, timeout)); // TODO: add back some extra checks outside of the realm ones? // var set = queryBeatmapSets().First(); diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 53e4ef07e7..5cbede54f5 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Collections.IO } // Name matches the automatically chosen name from `CleanRunHeadlessGameHost` above, so we end up using the same storage location. - using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName)) + using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName, null)) { try { diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 227314cffd..69dd2d930a 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -38,10 +38,10 @@ namespace osu.Game.Tests.Database [Test] public void TestDetachBeatmapSet() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using (var importer = new BeatmapModelManager(realmFactory, storage)) - using (new RulesetStore(realmFactory, storage)) + using (var importer = new BeatmapModelManager(realm, storage)) + using (new RulesetStore(realm, storage)) { ILive? beatmapSet; @@ -82,10 +82,10 @@ namespace osu.Game.Tests.Database [Test] public void TestUpdateDetachedBeatmapSet() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using (var importer = new BeatmapModelManager(realmFactory, storage)) - using (new RulesetStore(realmFactory, storage)) + using (var importer = new BeatmapModelManager(realm, storage)) + using (new RulesetStore(realm, storage)) { ILive? beatmapSet; @@ -139,53 +139,53 @@ namespace osu.Game.Tests.Database [Test] public void TestImportBeatmapThenCleanup() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using (var importer = new BeatmapModelManager(realmFactory, storage)) - using (new RulesetStore(realmFactory, storage)) + using (var importer = new BeatmapModelManager(realm, storage)) + using (new RulesetStore(realm, storage)) { ILive? imported; using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) imported = await importer.Import(reader); - Assert.AreEqual(1, realmFactory.Context.All().Count()); + Assert.AreEqual(1, realm.Realm.All().Count()); Assert.NotNull(imported); Debug.Assert(imported != null); imported.PerformWrite(s => s.DeletePending = true); - Assert.AreEqual(1, realmFactory.Context.All().Count(s => s.DeletePending)); + Assert.AreEqual(1, realm.Realm.All().Count(s => s.DeletePending)); } }); Logger.Log("Running with no work to purge pending deletions"); - RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All().Count()); }); + RunTestWithRealm((realm, _) => { Assert.AreEqual(0, realm.Realm.All().Count()); }); } [Test] public void TestImportWhenClosed() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - await LoadOszIntoStore(importer, realmFactory.Context); + await LoadOszIntoStore(importer, realm.Realm); }); } [Test] public void TestAccessFileAfterImport() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); var beatmap = imported.Beatmaps.First(); var file = beatmap.File; @@ -198,24 +198,24 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenDelete() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); }); } [Test] public void TestImportThenDeleteFromStream() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? tempPath = TestResources.GetTestBeatmapForImport(); @@ -224,7 +224,7 @@ namespace osu.Game.Tests.Database using (var stream = File.OpenRead(tempPath)) { importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath))); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); } Assert.NotNull(importedSet); @@ -233,39 +233,39 @@ namespace osu.Game.Tests.Database Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); File.Delete(tempPath); - var imported = realmFactory.Context.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); + var imported = realm.Realm.All().First(beatmapSet => beatmapSet.ID == importedSet.ID); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); }); } [Test] public void TestImportThenImport() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - checkBeatmapSetCount(realmFactory.Context, 1); - checkSingleReferencedFileCount(realmFactory.Context, 18); + checkBeatmapSetCount(realm.Realm, 1); + checkSingleReferencedFileCount(realm.Realm, 18); }); } [Test] public void TestImportThenImportWithReZip() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Database try { - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); string hashBefore = hashFile(temp); @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -311,10 +311,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenImportWithChangedHashedFile() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -323,9 +323,9 @@ namespace osu.Game.Tests.Database try { - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - await createScoreForBeatmap(realmFactory.Context, imported.Beatmaps.First()); + await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First()); using (var zip = ZipArchive.Open(temp)) zip.WriteToDirectory(extractedFolder); @@ -343,7 +343,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); // check the newly "imported" beatmap is not the original. Assert.NotNull(importedSecondTime); @@ -363,10 +363,10 @@ namespace osu.Game.Tests.Database [Ignore("intentionally broken by import optimisations")] public void TestImportThenImportWithChangedFile() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -375,7 +375,7 @@ namespace osu.Game.Tests.Database try { - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); using (var zip = ZipArchive.Open(temp)) zip.WriteToDirectory(extractedFolder); @@ -392,7 +392,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -411,10 +411,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenImportWithDifferentFilename() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -423,7 +423,7 @@ namespace osu.Game.Tests.Database try { - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); using (var zip = ZipArchive.Open(temp)) zip.WriteToDirectory(extractedFolder); @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Database var importedSecondTime = await importer.Import(new ImportTask(temp)); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.NotNull(importedSecondTime); Debug.Assert(importedSecondTime != null); @@ -460,12 +460,12 @@ namespace osu.Game.Tests.Database [Ignore("intentionally broken by import optimisations")] public void TestImportCorruptThenImport() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); var firstFile = imported.Files.First(); @@ -476,7 +476,7 @@ namespace osu.Game.Tests.Database using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create)) stream.WriteByte(0); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); using (var stream = storage.GetStream(firstFile.File.GetStoragePath())) Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import"); @@ -485,18 +485,18 @@ namespace osu.Game.Tests.Database Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); - checkBeatmapSetCount(realmFactory.Context, 1); - checkSingleReferencedFileCount(realmFactory.Context, 18); + checkBeatmapSetCount(realm.Realm, 1); + checkSingleReferencedFileCount(realm.Realm, 18); }); } [Test] public void TestModelCreationFailureDoesntReturn() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); var progressNotification = new ImportProgressNotification(); @@ -510,8 +510,8 @@ namespace osu.Game.Tests.Database new ImportTask(zipStream, string.Empty) ); - checkBeatmapSetCount(realmFactory.Context, 0); - checkBeatmapCount(realmFactory.Context, 0); + checkBeatmapSetCount(realm.Realm, 0); + checkBeatmapCount(realm.Realm, 0); Assert.IsEmpty(imported); Assert.AreEqual(ProgressNotificationState.Cancelled, progressNotification.State); @@ -521,7 +521,7 @@ namespace osu.Game.Tests.Database [Test] public void TestRollbackOnFailure() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { int loggedExceptionCount = 0; @@ -531,16 +531,16 @@ namespace osu.Game.Tests.Database Interlocked.Increment(ref loggedExceptionCount); }; - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - realmFactory.Context.Write(() => imported.Hash += "-changed"); + realm.Realm.Write(() => imported.Hash += "-changed"); - checkBeatmapSetCount(realmFactory.Context, 1); - checkBeatmapCount(realmFactory.Context, 12); - checkSingleReferencedFileCount(realmFactory.Context, 18); + checkBeatmapSetCount(realm.Realm, 1); + checkBeatmapCount(realm.Realm, 12); + checkSingleReferencedFileCount(realm.Realm, 18); string? brokenTempFilename = TestResources.GetTestBeatmapForImport(); @@ -565,10 +565,10 @@ namespace osu.Game.Tests.Database { } - checkBeatmapSetCount(realmFactory.Context, 1); - checkBeatmapCount(realmFactory.Context, 12); + checkBeatmapSetCount(realm.Realm, 1); + checkBeatmapCount(realm.Realm, 12); - checkSingleReferencedFileCount(realmFactory.Context, 18); + checkSingleReferencedFileCount(realm.Realm, 18); Assert.AreEqual(1, loggedExceptionCount); @@ -579,18 +579,18 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenDeleteThenImportOptimisedPath() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); Assert.IsTrue(imported.DeletePending); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); @@ -601,20 +601,52 @@ namespace osu.Game.Tests.Database } [Test] - public void TestImportThenDeleteThenImportNonOptimisedPath() + public void TestImportThenReimportAfterMissingFiles() { RunTestWithRealmAsync(async (realmFactory, storage) => { - using var importer = new NonOptimisedBeatmapImporter(realmFactory, storage); + using var importer = new BeatmapModelManager(realmFactory, storage); using var store = new RulesetStore(realmFactory, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realmFactory.Realm); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realmFactory.Realm); Assert.IsTrue(imported.DeletePending); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + // intentionally nuke all files + storage.DeleteDirectory("files"); + + Assert.That(imported.Files.All(f => !storage.GetStorageForDirectory("files").Exists(f.File.GetStoragePath()))); + + var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Realm); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + Assert.IsFalse(imported.DeletePending); + Assert.IsFalse(importedSecondTime.DeletePending); + + // check that the files now exist, even though they were deleted above. + Assert.That(importedSecondTime.Files.All(f => storage.GetStorageForDirectory("files").Exists(f.File.GetStoragePath()))); + }); + } + + [Test] + public void TestImportThenDeleteThenImportNonOptimisedPath() + { + RunTestWithRealmAsync(async (realm, storage) => + { + using var importer = new NonOptimisedBeatmapImporter(realm, storage); + using var store = new RulesetStore(realm, storage); + + var imported = await LoadOszIntoStore(importer, realm.Realm); + + deleteBeatmapSet(imported, realm.Realm); + + Assert.IsTrue(imported.DeletePending); + + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID == importedSecondTime.ID); @@ -627,22 +659,22 @@ namespace osu.Game.Tests.Database [Test] public void TestImportThenDeleteThenImportWithOnlineIDsMissing() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); - var imported = await LoadOszIntoStore(importer, realmFactory.Context); + var imported = await LoadOszIntoStore(importer, realm.Realm); - realmFactory.Context.Write(() => + realm.Realm.Write(() => { foreach (var b in imported.Beatmaps) b.OnlineID = -1; }); - deleteBeatmapSet(imported, realmFactory.Context); + deleteBeatmapSet(imported, realm.Realm); - var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); + var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) Assert.IsTrue(imported.ID != importedSecondTime.ID); @@ -653,10 +685,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWithDuplicateBeatmapIDs() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealm((realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); var metadata = new BeatmapMetadata { @@ -667,7 +699,7 @@ namespace osu.Game.Tests.Database } }; - var ruleset = realmFactory.Context.All().First(); + var ruleset = realm.Realm.All().First(); var toImport = new BeatmapSetInfo { @@ -686,7 +718,7 @@ namespace osu.Game.Tests.Database } }; - var imported = await importer.Import(toImport); + var imported = importer.Import(toImport); Assert.NotNull(imported); Debug.Assert(imported != null); @@ -699,15 +731,15 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWhenFileOpen() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); using (File.OpenRead(temp)) await importer.Import(temp); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); File.Delete(temp); Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); }); @@ -716,10 +748,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWithDuplicateHashes() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -740,7 +772,7 @@ namespace osu.Game.Tests.Database await importer.Import(temp); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); } finally { @@ -752,10 +784,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportNestedStructure() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -780,7 +812,7 @@ namespace osu.Game.Tests.Database Assert.NotNull(imported); Debug.Assert(imported != null); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder"); } @@ -794,10 +826,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportWithIgnoredDirectoryInArchive() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); @@ -830,7 +862,7 @@ namespace osu.Game.Tests.Database Assert.NotNull(imported); Debug.Assert(imported != null); - EnsureLoaded(realmFactory.Context); + EnsureLoaded(realm.Realm); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored"); Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder"); @@ -845,22 +877,22 @@ namespace osu.Game.Tests.Database [Test] public void TestUpdateBeatmapInfo() { - RunTestWithRealmAsync(async (realmFactory, storage) => + RunTestWithRealmAsync(async (realm, storage) => { - using var importer = new BeatmapModelManager(realmFactory, storage); - using var store = new RulesetStore(realmFactory, storage); + using var importer = new BeatmapModelManager(realm, storage); + using var store = new RulesetStore(realm, storage); string? temp = TestResources.GetTestBeatmapForImport(); await importer.Import(temp); // Update via the beatmap, not the beatmap info, to ensure correct linking - BeatmapSetInfo setToUpdate = realmFactory.Context.All().First(); + BeatmapSetInfo setToUpdate = realm.Realm.All().First(); var beatmapToUpdate = setToUpdate.Beatmaps.First(); - realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated"); + realm.Realm.Write(() => beatmapToUpdate.DifficultyName = "updated"); - BeatmapInfo updatedInfo = realmFactory.Context.All().First(b => b.ID == beatmapToUpdate.ID); + BeatmapInfo updatedInfo = realm.Realm.All().First(b => b.ID == beatmapToUpdate.ID); Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated")); }); } @@ -1004,8 +1036,8 @@ namespace osu.Game.Tests.Database public class NonOptimisedBeatmapImporter : BeatmapImporter { - public NonOptimisedBeatmapImporter(RealmContextFactory realmFactory, Storage storage) - : base(realmFactory, storage) + public NonOptimisedBeatmapImporter(RealmAccess realm, Storage storage) + : base(realm, storage) { } diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs index 3cb4705381..98b0ed99b5 100644 --- a/osu.Game.Tests/Database/FileStoreTests.cs +++ b/osu.Game.Tests/Database/FileStoreTests.cs @@ -19,10 +19,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportFile() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realmAccess, storage) => { - var realm = realmFactory.Context; - var files = new RealmFileStore(realmFactory, storage); + var realm = realmAccess.Realm; + var files = new RealmFileStore(realmAccess, storage); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); @@ -36,10 +36,10 @@ namespace osu.Game.Tests.Database [Test] public void TestImportSameFileTwice() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realmAccess, storage) => { - var realm = realmFactory.Context; - var files = new RealmFileStore(realmFactory, storage); + var realm = realmAccess.Realm; + var files = new RealmFileStore(realmAccess, storage); var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); @@ -53,10 +53,10 @@ namespace osu.Game.Tests.Database [Test] public void TestDontPurgeReferenced() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realmAccess, storage) => { - var realm = realmFactory.Context; - var files = new RealmFileStore(realmFactory, storage); + var realm = realmAccess.Realm; + var files = new RealmFileStore(realmAccess, storage); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); @@ -92,10 +92,10 @@ namespace osu.Game.Tests.Database [Test] public void TestPurgeUnreferenced() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realmAccess, storage) => { - var realm = realmFactory.Context; - var files = new RealmFileStore(realmFactory, storage); + var realm = realmAccess.Realm; + var files = new RealmFileStore(realmAccess, storage); var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 9ebe94b383..dc0d42595b 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -21,15 +21,15 @@ namespace osu.Game.Tests.Database [Test] public void TestConstructRealm() { - RunTestWithRealm((realmFactory, _) => { realmFactory.Run(realm => realm.Refresh()); }); + RunTestWithRealm((realm, _) => { realm.Run(r => r.Refresh()); }); } [Test] public void TestBlockOperations() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { } }); @@ -42,24 +42,25 @@ namespace osu.Game.Tests.Database [Test] public void TestNestedContextCreationWithSubscription() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { bool callbackRan = false; - realmFactory.Run(realm => + realm.RegisterCustomSubscription(r => { - var subscription = realm.All().QueryAsyncWithNotifications((sender, changes, error) => + var subscription = r.All().QueryAsyncWithNotifications((sender, changes, error) => { - realmFactory.Run(_ => + realm.Run(_ => { callbackRan = true; }); }); // Force the callback above to run. - realmFactory.Run(r => r.Refresh()); + realm.Run(rr => rr.Refresh()); subscription?.Dispose(); + return null; }); Assert.IsTrue(callbackRan); @@ -69,14 +70,14 @@ namespace osu.Game.Tests.Database [Test] public void TestBlockOperationsWithContention() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim(); ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim(); Task.Factory.StartNew(() => { - realmFactory.Run(_ => + realm.Run(_ => { hasThreadedUsage.Set(); @@ -86,15 +87,30 @@ namespace osu.Game.Tests.Database hasThreadedUsage.Wait(); + // Usually the host would run the synchronization context work per frame. + // For the sake of keeping this test simple (there's only one update invocation), + // let's replace it so we can ensure work is run immediately. + SynchronizationContext.SetSynchronizationContext(new ImmediateExecuteSynchronizationContext()); + Assert.Throws(() => { - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { } }); stopThreadedUsage.Set(); + + // Ensure we can block a second time after the usage has ended. + using (realm.BlockAllOperations()) + { + } }); } + + private class ImmediateExecuteSynchronizationContext : SynchronizationContext + { + public override void Post(SendOrPostCallback d, object? state) => d(state); + } } } diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 7b1cf763d6..2e3f708f79 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -21,11 +21,11 @@ namespace osu.Game.Tests.Database [Test] public void TestLiveEquality() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { - ILive beatmap = realmFactory.Run(realm => realm.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory)); + ILive beatmap = realm.Run(r => r.Write(_ => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realm)); - ILive beatmap2 = realmFactory.Run(realm => realm.All().First().ToLive(realmFactory)); + ILive beatmap2 = realm.Run(r => r.All().First().ToLive(realm)); Assert.AreEqual(beatmap, beatmap2); }); @@ -34,20 +34,20 @@ namespace osu.Game.Tests.Database [Test] public void TestAccessAfterStorageMigrate() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realm, storage) => { var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); ILive? liveBeatmap = null; - realmFactory.Run(realm => + realm.Run(r => { - realm.Write(r => r.Add(beatmap)); + r.Write(_ => r.Add(beatmap)); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { // recycle realm before migrating } @@ -66,13 +66,13 @@ namespace osu.Game.Tests.Database [Test] public void TestAccessAfterAttach() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(realmFactory); + var liveBeatmap = beatmap.ToLive(realm); - realmFactory.Run(realm => realm.Write(r => r.Add(beatmap))); + realm.Run(r => r.Write(_ => r.Add(beatmap))); Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); }); @@ -98,16 +98,16 @@ namespace osu.Game.Tests.Database [Test] public void TestScopedReadWithoutContext() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(threadContext => + realm.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -127,16 +127,16 @@ namespace osu.Game.Tests.Database [Test] public void TestScopedWriteWithoutContext() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(threadContext => + realm.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -153,10 +153,10 @@ namespace osu.Game.Tests.Database [Test] public void TestValueAccessNonManaged() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); - var liveBeatmap = beatmap.ToLive(realmFactory); + var liveBeatmap = beatmap.ToLive(realm); Assert.DoesNotThrow(() => { @@ -168,17 +168,17 @@ namespace osu.Game.Tests.Database [Test] public void TestValueAccessWithOpenContextFails() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(threadContext => + realm.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -193,7 +193,7 @@ namespace osu.Game.Tests.Database }); // Can't be used, even from within a valid context. - realmFactory.Run(threadContext => + realm.Run(threadContext => { Assert.Throws(() => { @@ -207,16 +207,16 @@ namespace osu.Game.Tests.Database [Test] public void TestValueAccessWithoutOpenContextFails() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(threadContext => + realm.Run(threadContext => { var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -235,18 +235,18 @@ namespace osu.Game.Tests.Database [Test] public void TestLiveAssumptions() { - RunTestWithRealm((realmFactory, _) => + RunTestWithRealm((realm, _) => { int changesTriggered = 0; - realmFactory.Run(outerRealm => + realm.RegisterCustomSubscription(outerRealm => { outerRealm.All().QueryAsyncWithNotifications(gotChange); ILive? liveBeatmap = null; Task.Factory.StartNew(() => { - realmFactory.Run(innerRealm => + realm.Run(innerRealm => { var ruleset = CreateRuleset(); var beatmap = innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); @@ -255,7 +255,7 @@ namespace osu.Game.Tests.Database // not just a refresh from the resolved Live. innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata()))); - liveBeatmap = beatmap.ToLive(realmFactory); + liveBeatmap = beatmap.ToLive(realm); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely(); @@ -282,6 +282,8 @@ namespace osu.Game.Tests.Database r.Remove(resolved); }); }); + + return null; }); void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs new file mode 100644 index 0000000000..d62ce3b585 --- /dev/null +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -0,0 +1,138 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Tests.Resources; +using Realms; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class RealmSubscriptionRegistrationTests : RealmTest + { + [Test] + public void TestSubscriptionWithContextLoss() + { + IEnumerable? resolvedItems = null; + ChangeSet? lastChanges = null; + + RunTestWithRealm((realm, _) => + { + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + + var registration = realm.RegisterForNotifications(r => r.All(), onChanged); + + testEventsArriving(true); + + // All normal until here. + // Now let's yank the main realm context. + resolvedItems = null; + lastChanges = null; + + using (realm.BlockAllOperations()) + Assert.That(resolvedItems, Is.Empty); + + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + + testEventsArriving(true); + + // Now let's try unsubscribing. + resolvedItems = null; + lastChanges = null; + + registration.Dispose(); + + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + + testEventsArriving(false); + + // And make sure even after another context loss we don't get firings. + using (realm.BlockAllOperations()) + Assert.That(resolvedItems, Is.Null); + + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + + testEventsArriving(false); + + void testEventsArriving(bool shouldArrive) + { + realm.Run(r => r.Refresh()); + + if (shouldArrive) + Assert.That(resolvedItems, Has.One.Items); + else + Assert.That(resolvedItems, Is.Null); + + realm.Write(r => + { + r.RemoveAll(); + r.RemoveAll(); + }); + + realm.Run(r => r.Refresh()); + + if (shouldArrive) + Assert.That(lastChanges?.DeletedIndices, Has.One.Items); + else + Assert.That(lastChanges, Is.Null); + } + }); + + void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + { + if (changes == null) + resolvedItems = sender; + + lastChanges = changes; + } + } + + [Test] + public void TestCustomRegisterWithContextLoss() + { + RunTestWithRealm((realm, _) => + { + BeatmapSetInfo? beatmapSetInfo = null; + + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + + var subscription = realm.RegisterCustomSubscription(r => + { + beatmapSetInfo = r.All().First(); + + return new InvokeOnDisposal(() => beatmapSetInfo = null); + }); + + Assert.That(beatmapSetInfo, Is.Not.Null); + + using (realm.BlockAllOperations()) + { + // custom disposal action fired when context lost. + Assert.That(beatmapSetInfo, Is.Null); + } + + // re-registration after context restore. + realm.Run(r => r.Refresh()); + Assert.That(beatmapSetInfo, Is.Not.Null); + + subscription.Dispose(); + + Assert.That(beatmapSetInfo, Is.Null); + + using (realm.BlockAllOperations()) + Assert.That(beatmapSetInfo, Is.Null); + + realm.Run(r => r.Refresh()); + Assert.That(beatmapSetInfo, Is.Null); + }); + } + } +} diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 0cee165f75..c2339dd9ad 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Database storage.DeleteDirectory(string.Empty); } - protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") + protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") { using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller)) { @@ -39,22 +39,22 @@ namespace osu.Game.Tests.Database // ReSharper disable once AccessToDisposedClosure var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller)); - using (var realmFactory = new RealmContextFactory(testStorage, "client")) + using (var realm = new RealmAccess(testStorage, "client")) { - Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - testAction(realmFactory, testStorage); + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}"); + testAction(realm, testStorage); - realmFactory.Dispose(); + realm.Dispose(); - Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); - realmFactory.Compact(); - Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); + Logger.Log($"Final database size: {getFileSize(testStorage, realm)}"); + realm.Compact(); + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realm)}"); } })); } } - protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "") + protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "") { using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller)) { @@ -62,15 +62,15 @@ namespace osu.Game.Tests.Database { var testStorage = storage.GetStorageForDirectory(caller); - using (var realmFactory = new RealmContextFactory(testStorage, "client")) + using (var realm = new RealmAccess(testStorage, "client")) { - Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - await testAction(realmFactory, testStorage); + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}"); + await testAction(realm, testStorage); - realmFactory.Dispose(); + realm.Dispose(); - Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); - realmFactory.Compact(); + Logger.Log($"Final database size: {getFileSize(testStorage, realm)}"); + realm.Compact(); } })); } @@ -138,11 +138,11 @@ namespace osu.Game.Tests.Database } } - private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory) + private static long getFileSize(Storage testStorage, RealmAccess realm) { try { - using (var stream = testStorage.GetStream(realmFactory.Filename)) + using (var stream = testStorage.GetStream(realm.Filename)) return stream?.Length ?? 0; } catch diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs index 4416da6f92..7544142b70 100644 --- a/osu.Game.Tests/Database/RulesetStoreTests.cs +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -12,37 +12,37 @@ namespace osu.Game.Tests.Database [Test] public void TestCreateStore() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realm, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); - Assert.AreEqual(4, realmFactory.Context.All().Count()); + Assert.AreEqual(4, realm.Realm.All().Count()); }); } [Test] public void TestCreateStoreTwiceDoesntAddRulesetsAgain() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realmFactory, storage); - var rulesets2 = new RulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realm, storage); + var rulesets2 = new RulesetStore(realm, storage); Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First()); - Assert.AreEqual(4, realmFactory.Context.All().Count()); + Assert.AreEqual(4, realm.Realm.All().Count()); }); } [Test] public void TestRetrievedRulesetsAreDetached() { - RunTestWithRealm((realmFactory, storage) => + RunTestWithRealm((realm, storage) => { - var rulesets = new RulesetStore(realmFactory, storage); + var rulesets = new RulesetStore(realm, storage); Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged); Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged); diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index c1041e9fd6..891801865f 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Database private RealmKeyBindingStore keyBindingStore; - private RealmContextFactory realmContextFactory; + private RealmAccess realm; [SetUp] public void SetUp() @@ -33,8 +33,8 @@ namespace osu.Game.Tests.Database storage = new NativeStorage(directory.FullName); - realmContextFactory = new RealmContextFactory(storage, "test"); - keyBindingStore = new RealmKeyBindingStore(realmContextFactory, new ReadableKeyCombinationProvider()); + realm = new RealmAccess(storage, "test"); + keyBindingStore = new RealmKeyBindingStore(realm, new ReadableKeyCombinationProvider()); } [Test] @@ -60,11 +60,11 @@ namespace osu.Game.Tests.Database KeyBindingContainer testContainer = new TestKeyBindingContainer(); // Add some excess bindings for an action which only supports 1. - realmContextFactory.Write(realm => + realm.Write(r => { - realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A))); - realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S))); - realm.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D))); + r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.A))); + r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.S))); + r.Add(new RealmKeyBinding(GlobalAction.Back, new KeyCombination(InputKey.D))); }); Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3)); @@ -76,9 +76,9 @@ namespace osu.Game.Tests.Database private int queryCount(GlobalAction? match = null) { - return realmContextFactory.Run(realm => + return realm.Run(r => { - var results = realm.All(); + var results = r.All(); if (match.HasValue) results = results.Where(k => k.ActionInt == (int)match.Value); return results.Count(); @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer, Enumerable.Empty()); - realmContextFactory.Run(outerRealm => + realm.Run(outerRealm => { var backBinding = outerRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Database var tsr = ThreadSafeReference.Create(backBinding); - realmContextFactory.Run(innerRealm => + realm.Run(innerRealm => { var binding = innerRealm.ResolveReference(tsr); innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Database [TearDown] public void TearDown() { - realmContextFactory.Dispose(); + realm.Dispose(); storage.DeleteDirectory(string.Empty); } diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index f0ebd7a8cc..88862ea28b 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -261,7 +261,7 @@ namespace osu.Game.Tests.Gameplay public AudioManager AudioManager => Audio; public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; - public RealmContextFactory RealmContextFactory => null; + public RealmAccess RealmAccess => null; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; #endregion diff --git a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs index 834c05fd08..6ae8231deb 100644 --- a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs +++ b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.NonVisual const int beat_length_numerator = 2000; const int beat_length_denominator = 7; - const TimeSignatures signature = TimeSignatures.SimpleQuadruple; + TimeSignature signature = TimeSignature.SimpleQuadruple; var beatmap = new Beatmap { @@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual for (int i = 0; i * beat_length_denominator < barLines.Count; i++) { var barLine = barLines[i * beat_length_denominator]; - int expectedTime = beat_length_numerator * (int)signature * i; + int expectedTime = beat_length_numerator * signature.Numerator * i; // every seventh bar's start time should be at least greater than the whole number we expect. // It cannot be less, as that can affect overlapping scroll algorithms @@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime)); // check major/minor lines for good measure too - Assert.AreEqual(i % (int)signature == 0, barLine.Major); + Assert.AreEqual(i % signature.Numerator == 0, barLine.Major); } } diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 61ef31e07e..834930a05e 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -142,19 +142,28 @@ namespace osu.Game.Tests.NonVisual Assert.That(osuStorage, Is.Not.Null); + // In the following tests, realm files are ignored as + // - in the case of checking the source, interacting with the pipe files (client.realm.note) may + // lead to unexpected behaviour. + // - in the case of checking the destination, the files may have already been recreated by the game + // as part of the standard migration flow. + foreach (string 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)) + if (!file.Contains("realm", StringComparison.Ordinal)) + { Assert.That(File.Exists(Path.Combine(originalDirectory, file))); - Assert.That(storage.Exists(file), Is.False); + Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored"); + } } foreach (string dir in osuStorage.IgnoreDirectories) { - Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir))); - Assert.That(storage.ExistsDirectory(dir), Is.False); + if (!dir.Contains("realm", StringComparison.Ordinal)) + { + Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir))); + Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored"); + } } Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}")); diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 1d639c6418..95c15367aa 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -45,8 +45,8 @@ namespace osu.Game.Tests.Online [BackgroundDependencyLoader] private void load(AudioManager audio, GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API, host)); } @@ -60,8 +60,8 @@ namespace osu.Game.Tests.Online testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapSet = testBeatmapInfo.BeatmapSet; - ContextFactory.Write(r => r.RemoveAll()); - ContextFactory.Write(r => r.RemoveAll()); + Realm.Write(r => r.RemoveAll()); + Realm.Write(r => r.RemoveAll()); selectedItem.Value = new PlaylistItem { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Online addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); - AddUntilStep("wait for import", () => beatmaps.CurrentImportTask?.IsCompleted == true); + AddUntilStep("wait for import", () => beatmaps.CurrentImport != null); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); } @@ -164,32 +164,32 @@ namespace osu.Game.Tests.Online { public TaskCompletionSource AllowImport = new TaskCompletionSource(); - public Task> CurrentImportTask { get; private set; } + public ILive CurrentImport { get; private set; } - public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) - : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) + public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap) { } - protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) { - return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, onlineLookupQueue); + return new TestBeatmapModelManager(this, storage, realm, rulesets, onlineLookupQueue); } internal class TestBeatmapModelManager : BeatmapModelManager { private readonly TestBeatmapManager testBeatmapManager; - public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) - : base(databaseContextFactory, storage, beatmapOnlineLookupQueue) + public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) + : base(databaseAccess, storage, beatmapOnlineLookupQueue) { this.testBeatmapManager = testBeatmapManager; } - public override async Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public override ILive Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { - await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); - return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); + testBeatmapManager.AllowImport.Task.WaitSafely(); + return (testBeatmapManager.CurrentImport = base.Import(item, archive, lowPriority, cancellationToken)); } } } diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index d2cab09ac9..81b624f908 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -80,7 +80,10 @@ namespace osu.Game.Tests.Resources public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null) { int j = 0; - RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length] ?? new OsuRuleset().RulesetInfo; + + rulesets ??= new[] { new OsuRuleset().RulesetInfo }; + + RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length]; int setId = Interlocked.Increment(ref importId); diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index dd12c94855..8de9f0a292 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; @@ -25,7 +24,7 @@ namespace osu.Game.Tests.Scores.IO public class ImportScoreTest : ImportTest { [Test] - public async Task TestBasicImport() + public void TestBasicImport() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -49,7 +48,7 @@ namespace osu.Game.Tests.Scores.IO BeatmapInfo = beatmap.Beatmaps.First() }; - var imported = await LoadScoreIntoOsu(osu, toImport); + var imported = LoadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.TotalScore, imported.TotalScore); @@ -67,7 +66,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public async Task TestImportMods() + public void TestImportMods() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -85,7 +84,7 @@ namespace osu.Game.Tests.Scores.IO Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; - var imported = await LoadScoreIntoOsu(osu, toImport); + var imported = LoadScoreIntoOsu(osu, toImport); Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); @@ -98,7 +97,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public async Task TestImportStatistics() + public void TestImportStatistics() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -120,7 +119,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = await LoadScoreIntoOsu(osu, toImport); + var imported = LoadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); @@ -133,7 +132,7 @@ namespace osu.Game.Tests.Scores.IO } [Test] - public async Task TestOnlineScoreIsAvailableLocally() + public void TestOnlineScoreIsAvailableLocally() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { @@ -143,7 +142,7 @@ namespace osu.Game.Tests.Scores.IO var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); - await LoadScoreIntoOsu(osu, new ScoreInfo + LoadScoreIntoOsu(osu, new ScoreInfo { User = new APIUser { Username = "Test user" }, BeatmapInfo = beatmap.Beatmaps.First(), @@ -168,13 +167,14 @@ namespace osu.Game.Tests.Scores.IO } } - public static async Task LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) + public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { // clone to avoid attaching the input score to realm. score = score.DeepClone(); var scoreManager = osu.Dependencies.Get(); - await scoreManager.Import(score, archive); + + scoreManager.Import(score, archive); return scoreManager.Query(_ => true); } diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 4ab4c08353..40e7c0a844 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -47,10 +47,10 @@ namespace osu.Game.Tests.Visual.Background [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(Realm); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 18572ac211..d4c13059da 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Collections [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs index 243bb71e26..cf6488f721 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; +using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps.IO; namespace osu.Game.Tests.Visual.Editing @@ -37,11 +38,8 @@ namespace osu.Game.Tests.Visual.Editing base.SetUpSteps(); } - protected override void LoadEditor() - { - Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First()); - base.LoadEditor(); - } + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First()); [Test] public void TestBasicSwitch() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 2386446e96..e3fb44534b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Setup; +using osu.Game.Storyboards; using osu.Game.Tests.Resources; using SharpCompress.Archives; using SharpCompress.Archives.Zip; @@ -39,11 +40,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); } - protected override void LoadEditor() - { - Beatmap.Value = new DummyWorkingBeatmap(Audio, null); - base.LoadEditor(); - } + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new DummyWorkingBeatmap(Audio, null); [Test] public void TestCreateNewBeatmap() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index bf3b46c6f7..96f815621c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; @@ -40,6 +41,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("Set beat divisor", () => editor.Dependencies.Get().Value = 16); AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7); AddStep("Set artist and title", () => { @@ -88,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing private void checkMutations() { AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + AddAssert("Beatmap has correct beat divisor", () => editorBeatmap.BeatmapInfo.BeatDivisor == 16); AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); AddAssert("Beatmap has correct author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username == "author"); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs index bb630e5d5c..79afc8cf27 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs @@ -17,6 +17,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Play; +using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps.IO; using osuTK.Graphics; using osuTK.Input; @@ -43,9 +44,11 @@ namespace osu.Game.Tests.Visual.Editing base.SetUpSteps(); } + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0)); + protected override void LoadEditor() { - Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0)); SelectedMods.Value = new[] { new ModCinema() }; base.LoadEditor(); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs new file mode 100644 index 0000000000..b34974dfc7 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneLabelledTimeSignature.cs @@ -0,0 +1,88 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Timing; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneLabelledTimeSignature : OsuManualInputManagerTestScene + { + private LabelledTimeSignature timeSignature; + + private void createLabelledTimeSignature(TimeSignature initial) => AddStep("create labelled time signature", () => + { + Child = timeSignature = new LabelledTimeSignature + { + Label = "Time Signature", + RelativeSizeAxes = Axes.None, + Width = 400, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { Value = initial } + }; + }); + + private OsuTextBox numeratorTextBox => timeSignature.ChildrenOfType().Single(); + + [Test] + public void TestInitialValue() + { + createLabelledTimeSignature(TimeSignature.SimpleTriple); + AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple)); + } + + [Test] + public void TestChangeViaCurrent() + { + createLabelledTimeSignature(TimeSignature.SimpleQuadruple); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("set current to 5/4", () => timeSignature.Current.Value = new TimeSignature(5)); + + AddAssert("current is 5/4", () => timeSignature.Current.Value.Equals(new TimeSignature(5))); + AddAssert("numerator is 5", () => numeratorTextBox.Current.Value == "5"); + + AddStep("set current to 3/4", () => timeSignature.Current.Value = TimeSignature.SimpleTriple); + + AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple)); + AddAssert("numerator is 3", () => numeratorTextBox.Current.Value == "3"); + } + + [Test] + public void TestChangeNumerator() + { + createLabelledTimeSignature(TimeSignature.SimpleQuadruple); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox)); + + AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7"); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("drop focus", () => InputManager.ChangeFocus(null)); + AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7))); + } + + [Test] + public void TestInvalidChangeRollbackOnCommit() + { + createLabelledTimeSignature(TimeSignature.SimpleQuadruple); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox)); + + AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0"); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + + AddStep("drop focus", () => InputManager.ChangeFocus(null)); + AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); + AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4"); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs index 951ee1489d..759e4fa4ec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay Add(new ModNightcore.NightcoreBeatContainer()); - AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleQuadruple)); - AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleTriple)); + AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignature.SimpleQuadruple)); + AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignature.SimpleTriple)); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 8199389b36..a12171401a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -147,7 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); - AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true)).GetResultSafely()); + AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true))); AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs index 4754a73f83..642cc68de5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs @@ -8,6 +8,8 @@ using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays.Login; +using osu.Game.Users.Drawables; +using osuTK.Input; namespace osu.Game.Tests.Visual.Menus { @@ -15,6 +17,7 @@ namespace osu.Game.Tests.Visual.Menus public class TestSceneLoginPanel : OsuManualInputManagerTestScene { private LoginPanel loginPanel; + private int hideCount; [SetUpSteps] public void SetUpSteps() @@ -26,6 +29,7 @@ namespace osu.Game.Tests.Visual.Menus Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 0.5f, + RequestHide = () => hideCount++, }); }); } @@ -51,5 +55,22 @@ namespace osu.Game.Tests.Visual.Menus AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); } + + [Test] + public void TestClickingOnFlagClosesPanel() + { + AddStep("reset hide count", () => hideCount = 0); + + AddStep("logout", () => API.Logout()); + AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + + AddStep("click on flag", () => + { + InputManager.MoveMouseTo(loginPanel.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("hide requested", () => hideCount == 1); + } } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 3ebc64cd0b..10a82089b3 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Menus Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null; // ensure we have at least two beatmaps available to identify the direction the music controller navigated to. - AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()).WaitSafely(), 5); + AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5); AddStep("import beatmap with track", () => { diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index d4282ff21e..4cd19b53a4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 99c867b014..92accb0cd1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -8,7 +8,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; @@ -43,9 +42,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } [Test] @@ -158,7 +157,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Debug.Assert(beatmap.BeatmapSet != null); - AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet).GetResultSafely()); + AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet)); createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach())); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 373b165acc..3f151a0ae8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -61,9 +61,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 15ebe0ee00..5465061891 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -42,9 +42,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmaps = new List(); @@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Multiplayer beatmapSetInfo.Beatmaps.Add(beatmap); } - manager.Import(beatmapSetInfo).WaitSafely(); + manager.Import(beatmapSetInfo); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 012a2fd960..9d14d80d07 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -38,9 +38,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index d547b42891..d970ab6c34 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -33,9 +33,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } [SetUp] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 965b142ed7..d83421ee3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -38,9 +38,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 1c346e09d5..9867e5225e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -40,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 221732910b..42ae279667 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -41,9 +41,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 0b0006e437..d933491ab6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Utils; @@ -34,13 +33,13 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); - manager.Import(beatmapSet).WaitSafely(); + manager.Import(beatmapSet); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 39cde0ad87..73c67d26d9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -42,9 +42,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } public override void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 0f314242b4..bfcefdbbfe 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Navigation .ChildrenOfType().SingleOrDefault(); private RealmKeyBinding firstOsuRulesetKeyBindings => Game.Dependencies - .Get().Context + .Get().Realm .All() .AsEnumerable() .First(k => k.RulesetName == "osu" && k.ActionInt == 0); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index f6c53e76c4..63226de750 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Extensions; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -125,7 +124,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, } - }).GetResultSafely()?.Value; + })?.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 7bd8110374..7656bf79dc 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using NUnit.Framework; -using osu.Framework.Extensions; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -60,7 +59,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = new OsuRuleset().RulesetInfo }, } - }).GetResultSafely()?.Value; + })?.Value; }); } @@ -135,7 +134,7 @@ namespace osu.Game.Tests.Visual.Navigation BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, User = new GuestUser(), - }).GetResultSafely().Value; + }).Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index bc9f759bdd..68225f6d64 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -40,9 +39,9 @@ namespace osu.Game.Tests.Visual.Playlists [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); } [SetUpSteps] @@ -151,7 +150,7 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(modifiedBeatmap.BeatmapInfo.BeatmapSet != null); - manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet).WaitSafely(); + manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet); }); // Create the room using the real beatmap values. @@ -196,7 +195,7 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(originalBeatmap.BeatmapInfo.BeatmapSet != null); - manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet).WaitSafely(); + manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet); }); AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash); @@ -219,7 +218,7 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); - importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet).GetResultSafely()?.Value.Detach(); + importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)?.Value.Detach(); }); private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index a77480ee54..988f429ff5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -36,17 +36,17 @@ namespace osu.Game.Tests.Visual.Ranking private BeatmapManager beatmaps { get; set; } [Resolved] - private RealmContextFactory realmContextFactory { get; set; } + private RealmAccess realm { get; set; } protected override void LoadComplete() { base.LoadComplete(); - realmContextFactory.Run(realm => + realm.Run(r => { - var beatmapInfo = realm.All() - .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0) - .FirstOrDefault(); + var beatmapInfo = r.All() + .Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0) + .FirstOrDefault(); if (beatmapInfo != null) Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 2e1a66be5f..e31be1d51a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -42,10 +42,10 @@ namespace osu.Game.Tests.Visual.SongSelect { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); - Dependencies.Cache(ContextFactory); + dependencies.Cache(rulesetStore = new RulesetStore(Realm)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler)); + Dependencies.Cache(Realm); return dependencies; } @@ -180,7 +180,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep(@"Load new scores via manager", () => { foreach (var score in generateSampleScores(beatmapInfo())) - scoreManager.Import(score).WaitSafely(); + scoreManager.Import(score); }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index b7bc0c37e1..940d001c5b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Extensions; @@ -184,7 +183,7 @@ namespace osu.Game.Tests.Visual.SongSelect beatmap.DifficultyName = $"SR{i + 1}"; } - return Game.BeatmapManager.Import(beatmapSet).GetResultSafely()?.Value; + return Game.BeatmapManager.Import(beatmapSet)?.Value; } private bool ensureAllBeatmapSetsImported(IEnumerable beatmapSets) => beatmapSets.All(set => set != null); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index ca8e9d2eff..b384061531 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6295a52bdd..630171a4d0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -47,9 +46,9 @@ namespace osu.Game.Tests.Visual.SongSelect { // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // At a point we have isolated interactive test runs enough, this can likely be removed. - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(ContextFactory); - Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(Realm); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); Dependencies.Cache(music = new MusicController()); @@ -260,7 +259,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)).WaitSafely(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)); }); } else @@ -675,7 +674,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)).WaitSafely(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)); }); int previousSetID = 0; @@ -715,7 +714,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)).WaitSafely(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)); }); DrawableCarouselBeatmapSet set = null; @@ -764,7 +763,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); - imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets)).GetResultSafely()?.Value; + imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets))?.Value; }); AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); @@ -873,7 +872,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id)); - private void importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray())).WaitSafely(); + private void importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray())); private void checkMusicPlaying(bool playing) => AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing); @@ -903,7 +902,7 @@ namespace osu.Game.Tests.Visual.SongSelect var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); for (int i = 0; i < 10; i++) - manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)).WaitSafely(); + manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)); }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs index 3aa5a759e6..8e5f76a2eb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs @@ -28,10 +28,10 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); - Dependencies.Cache(ContextFactory); + Dependencies.Cache(rulesets = new RulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler)); + Dependencies.Cache(Realm); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index f43354514b..4826d2fb33 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.UserInterface private BeatmapInfo beatmapInfo; [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } [Cached] private readonly DialogOverlay dialogOverlay; @@ -87,10 +87,10 @@ namespace osu.Game.Tests.Visual.UserInterface { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); - dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, ContextFactory, Scheduler)); - Dependencies.Cache(ContextFactory); + dependencies.Cache(rulesetStore = new RulesetStore(Realm)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler)); + Dependencies.Cache(Realm); var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); @@ -112,7 +112,7 @@ namespace osu.Game.Tests.Visual.UserInterface Ruleset = new OsuRuleset().RulesetInfo, }; - importedScores.Add(scoreManager.Import(score).GetResultSafely().Value); + importedScores.Add(scoreManager.Import(score).Value); } }); @@ -122,10 +122,10 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void Setup() => Schedule(() => { - realmFactory.Run(realm => + realm.Run(r => { // Due to soft deletions, we can re-use deleted scores between test runs - scoreManager.Undelete(realm.All().Where(s => s.DeletePending).ToList()); + scoreManager.Undelete(r.All().Where(s => s.DeletePending).ToList()); }); leaderboard.Scores = null; diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index fc5d3b652f..26fb03bed4 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tournament.Tests.NonVisual [Test] public void TestCustomDirectory() { - using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file. + using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestCustomDirectory), null)) // don't use clean run as we are writing a config file. { string osuDesktopStorage = Path.Combine(host.UserStoragePaths.First(), nameof(TestCustomDirectory)); const string custom_tournament = "custom"; @@ -68,7 +68,7 @@ namespace osu.Game.Tournament.Tests.NonVisual [Test] public void TestMigration() { - using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestMigration))) // don't use clean run as we are writing test files for migration. + using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestMigration), null)) // don't use clean run as we are writing test files for migration. { string osuRoot = Path.Combine(host.UserStoragePaths.First(), nameof(TestMigration)); string configFile = Path.Combine(osuRoot, "tournament.ini"); diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs index 03252e3be6..80cc9be5c1 100644 --- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tournament.Tests.NonVisual public void CheckIPCLocation() { // don't use clean run because files are being written before osu! launches. - using (var host = new TestRunHeadlessGameHost(nameof(CheckIPCLocation))) + using (var host = new TestRunHeadlessGameHost(nameof(CheckIPCLocation), null)) { string basePath = Path.Combine(host.UserStoragePaths.First(), nameof(CheckIPCLocation)); diff --git a/osu.Game.Tournament.Tests/TournamentTestRunner.cs b/osu.Game.Tournament.Tests/TournamentTestRunner.cs index 1f63f7c545..229ab41a1e 100644 --- a/osu.Game.Tournament.Tests/TournamentTestRunner.cs +++ b/osu.Game.Tournament.Tests/TournamentTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tournament.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true })) { host.Run(new TournamentTestBrowser()); return 0; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 43e4b482bd..a1c1982f00 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Audio; using osu.Framework.Audio.Track; -using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Testing; @@ -41,11 +40,11 @@ namespace osu.Game.Beatmaps private readonly WorkingBeatmapCache workingBeatmapCache; private readonly BeatmapOnlineLookupQueue? onlineBeatmapLookupQueue; - private readonly RealmContextFactory contextFactory; + private readonly RealmAccess realm; - public BeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false) + public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false) { - this.contextFactory = contextFactory; + this.realm = realm; if (performOnlineLookups) { @@ -55,11 +54,11 @@ namespace osu.Game.Beatmaps onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); } - var userResources = new RealmFileStore(contextFactory, storage).Store; + var userResources = new RealmFileStore(realm, storage).Store; BeatmapTrackStore = audioManager.GetTrackStore(userResources); - beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, onlineBeatmapLookupQueue); + beatmapModelManager = CreateBeatmapModelManager(storage, realm, rulesets, onlineBeatmapLookupQueue); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host); beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; @@ -70,8 +69,8 @@ namespace osu.Game.Beatmaps return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host); } - protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) => - new BeatmapModelManager(contextFactory, storage, onlineLookupQueue); + protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue? onlineLookupQueue) => + new BeatmapModelManager(realm, storage, onlineLookupQueue); /// /// Create a new . @@ -105,7 +104,7 @@ namespace osu.Game.Beatmaps foreach (BeatmapInfo b in beatmapSet.Beatmaps) b.BeatmapSet = beatmapSet; - var imported = beatmapModelManager.Import(beatmapSet).GetResultSafely(); + var imported = beatmapModelManager.Import(beatmapSet); if (imported == null) throw new InvalidOperationException("Failed to import new beatmap"); @@ -119,12 +118,12 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to hide. public void Hide(BeatmapInfo beatmapInfo) { - contextFactory.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = realm.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID); beatmapInfo.Hidden = true; transaction.Commit(); @@ -138,12 +137,12 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmapInfo) { - contextFactory.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { if (!beatmapInfo.IsManaged) - beatmapInfo = realm.Find(beatmapInfo.ID); + beatmapInfo = r.Find(beatmapInfo.ID); beatmapInfo.Hidden = false; transaction.Commit(); @@ -153,11 +152,11 @@ namespace osu.Game.Beatmaps public void RestoreAll() { - contextFactory.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { - foreach (var beatmap in realm.All().Where(b => b.Hidden)) + foreach (var beatmap in r.All().Where(b => b.Hidden)) beatmap.Hidden = false; transaction.Commit(); @@ -171,10 +170,10 @@ namespace osu.Game.Beatmaps /// A list of available . public List GetAllUsableBeatmapSets() { - return contextFactory.Run(realm => + return realm.Run(r => { - realm.Refresh(); - return realm.All().Where(b => !b.DeletePending).Detach(); + r.Refresh(); + return r.All().Where(b => !b.DeletePending).Detach(); }); } @@ -185,7 +184,7 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public ILive? QueryBeatmapSet(Expression> query) { - return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.ToLive(contextFactory)); + return realm.Run(r => r.All().FirstOrDefault(query)?.ToLive(realm)); } #region Delegation to BeatmapModelManager (methods which previously existed locally). @@ -240,9 +239,9 @@ namespace osu.Game.Beatmaps public void Delete(Expression>? filter = null, bool silent = false) { - contextFactory.Run(realm => + realm.Run(r => { - var items = realm.All().Where(s => !s.DeletePending && !s.Protected); + var items = r.All().Where(s => !s.DeletePending && !s.Protected); if (filter != null) items = items.Where(filter); @@ -253,7 +252,7 @@ namespace osu.Game.Beatmaps public void UndeleteAll() { - contextFactory.Run(realm => beatmapModelManager.Undelete(realm.All().Where(s => s.DeletePending).ToList())); + realm.Run(r => beatmapModelManager.Undelete(r.All().Where(s => s.DeletePending).ToList())); } public void Undelete(List items, bool silent = false) @@ -295,7 +294,7 @@ namespace osu.Game.Beatmaps return beatmapModelManager.Import(archive, lowPriority, cancellationToken); } - public Task?> Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public ILive? Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); } @@ -312,9 +311,9 @@ namespace osu.Game.Beatmaps // If we seem to be missing files, now is a good time to re-fetch. if (importedBeatmap?.BeatmapSet?.Files.Count == 0) { - contextFactory.Run(realm => + realm.Run(r => { - var refetch = realm.Find(importedBeatmap.ID)?.Detach(); + var refetch = r.Find(importedBeatmap.ID)?.Detach(); if (refetch != null) importedBeatmap = refetch; diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index ead86c1059..e8104f2ecb 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -33,8 +33,8 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - public BeatmapModelManager(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) - : base(contextFactory, storage, onlineLookupQueue) + public BeatmapModelManager(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) + : base(realm, storage, onlineLookupQueue) { } @@ -98,12 +98,12 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) { - return ContextFactory.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); + return Realm.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); } public void Update(BeatmapSetInfo item) { - ContextFactory.Write(realm => + Realm.Write(realm => { var existing = realm.Find(item.ID); item.CopyChangesToRealm(existing); diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index ec20328fab..922439fcb8 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time signature at this control point. /// - public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple }; + public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignature.SimpleQuadruple); /// /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing. @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time signature at this control point. /// - public TimeSignatures TimeSignature + public TimeSignature TimeSignature { get => TimeSignatureBindable.Value; set => TimeSignatureBindable.Value = value; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 893eb8ab78..8f3f05aa9f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -340,9 +340,9 @@ namespace osu.Game.Beatmaps.Formats double beatLength = Parsing.ParseDouble(split[1].Trim()); double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; - TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; + TimeSignature timeSignature = TimeSignature.SimpleQuadruple; if (split.Length >= 3) - timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]); + timeSignature = split[2][0] == '0' ? TimeSignature.SimpleQuadruple : new TimeSignature(Parsing.ParseInt(split[2])); LegacySampleBank sampleSet = defaultSampleBank; if (split.Length >= 4) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index ebdc882d2f..9d848fd8a4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps.Formats if (effectPoint.OmitFirstBarLine) effectFlags |= LegacyEffectFlags.OmitFirstBarLine; - writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},")); + writer.Write(FormattableString.Invariant($"{legacyControlPoints.TimingPointAt(time).TimeSignature.Numerator},")); writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); @@ -242,12 +242,7 @@ namespace osu.Game.Beatmaps.Formats yield break; foreach (var hitObject in hitObjects) - { yield return hitObject.DifficultyControlPoint; - - foreach (var nested in collectDifficultyControlPoints(hitObject.NestedHitObjects)) - yield return nested; - } } void extractDifficultyControlPoints(IEnumerable hitObjects) diff --git a/osu.Game/Beatmaps/Timing/TimeSignature.cs b/osu.Game/Beatmaps/Timing/TimeSignature.cs new file mode 100644 index 0000000000..eebbcc34cd --- /dev/null +++ b/osu.Game/Beatmaps/Timing/TimeSignature.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Beatmaps.Timing +{ + /// + /// Stores the time signature of a track. + /// For now, the lower numeral can only be 4; support for other denominators can be considered at a later date. + /// + public class TimeSignature : IEquatable + { + /// + /// The numerator of a signature. + /// + public int Numerator { get; } + + // TODO: support time signatures with a denominator other than 4 + // this in particular requires a new beatmap format. + + public TimeSignature(int numerator) + { + if (numerator < 1) + throw new ArgumentOutOfRangeException(nameof(numerator), numerator, "The numerator of a time signature must be positive."); + + Numerator = numerator; + } + + public static TimeSignature SimpleTriple { get; } = new TimeSignature(3); + public static TimeSignature SimpleQuadruple { get; } = new TimeSignature(4); + + public override string ToString() => $"{Numerator}/4"; + + public bool Equals(TimeSignature other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Numerator == other.Numerator; + } + + public override int GetHashCode() => Numerator; + } +} diff --git a/osu.Game/Beatmaps/Timing/TimeSignatures.cs b/osu.Game/Beatmaps/Timing/TimeSignatures.cs index 33e6342ae6..d783d3f9ec 100644 --- a/osu.Game/Beatmaps/Timing/TimeSignatures.cs +++ b/osu.Game/Beatmaps/Timing/TimeSignatures.cs @@ -1,11 +1,13 @@ // 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; namespace osu.Game.Beatmaps.Timing { - public enum TimeSignatures + [Obsolete("Use osu.Game.Beatmaps.Timing.TimeSignature instead.")] + public enum TimeSignatures // can be removed 20220722 { [Description("4/4")] SimpleQuadruple = 4, diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 6947752c47..d3f356bb24 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -100,7 +100,7 @@ namespace osu.Game.Beatmaps TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; AudioManager IStorageResourceProvider.AudioManager => audioManager; - RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; + RealmAccess IStorageResourceProvider.RealmAccess => null; IResourceStore IStorageResourceProvider.Files => files; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index 2bba20fb09..e5d2d572c8 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -10,11 +10,11 @@ namespace osu.Game.Configuration // this class mostly exists as a wrapper to avoid breaking the ruleset API (see usage in RulesetConfigManager). // it may cease to exist going forward, depending on how the structure of the config data layer changes. - public readonly RealmContextFactory Realm; + public readonly RealmAccess Realm; - public SettingsStore(RealmContextFactory realmFactory) + public SettingsStore(RealmAccess realm) { - Realm = realmFactory; + Realm = realm; } } } diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs index bbbdac352e..adf91e4a41 100644 --- a/osu.Game/Database/EFToRealmMigrator.cs +++ b/osu.Game/Database/EFToRealmMigrator.cs @@ -27,13 +27,15 @@ namespace osu.Game.Database { internal class EFToRealmMigrator : CompositeDrawable { - public bool FinishedMigrating { get; private set; } + public Task MigrationCompleted => migrationCompleted.Task; + + private readonly TaskCompletionSource migrationCompleted = new TaskCompletionSource(); [Resolved] private DatabaseContextFactory efContextFactory { get; set; } = null!; [Resolved] - private RealmContextFactory realmContextFactory { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -99,6 +101,17 @@ namespace osu.Game.Database { using (var ef = efContextFactory.Get()) { + realm.Write(r => + { + // Before beginning, ensure realm is in an empty state. + // Migrations which are half-completed could lead to issues if the user tries a second time. + // Note that we only do this for beatmaps and scores since the other migrations are yonks old. + r.RemoveAll(); + r.RemoveAll(); + r.RemoveAll(); + r.RemoveAll(); + }); + migrateSettings(ef); migrateSkins(ef); migrateBeatmaps(ef); @@ -114,7 +127,7 @@ namespace osu.Game.Database Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important); }, TaskCreationOptions.LongRunning).ContinueWith(t => { - FinishedMigrating = true; + migrationCompleted.SetResult(true); }); } @@ -145,91 +158,82 @@ namespace osu.Game.Database int count = existingBeatmapSets.Count(); - realmContextFactory.Run(realm => + realm.Run(r => { log($"Found {count} beatmaps in EF"); - // only migrate data if the realm database is empty. - // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. - if (realm.All().Any(s => !s.Protected)) - { - log("Skipping migration as realm already has beatmaps loaded"); - } - else - { - var transaction = realm.BeginWrite(); - int written = 0; + var transaction = r.BeginWrite(); + int written = 0; - try + try + { + foreach (var beatmapSet in existingBeatmapSets) { - foreach (var beatmapSet in existingBeatmapSets) + if (++written % 1000 == 0) { - if (++written % 1000 == 0) - { - transaction.Commit(); - transaction = realm.BeginWrite(); - log($"Migrated {written}/{count} beatmaps..."); - } + transaction.Commit(); + transaction = r.BeginWrite(); + log($"Migrated {written}/{count} beatmaps..."); + } - var realmBeatmapSet = new BeatmapSetInfo + var realmBeatmapSet = new BeatmapSetInfo + { + OnlineID = beatmapSet.OnlineID ?? -1, + DateAdded = beatmapSet.DateAdded, + Status = beatmapSet.Status, + DeletePending = beatmapSet.DeletePending, + Hash = beatmapSet.Hash, + Protected = beatmapSet.Protected, + }; + + migrateFiles(beatmapSet, r, realmBeatmapSet); + + foreach (var beatmap in beatmapSet.Beatmaps) + { + var ruleset = r.Find(beatmap.RulesetInfo.ShortName); + var metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata); + + var realmBeatmap = new BeatmapInfo(ruleset, new BeatmapDifficulty(beatmap.BaseDifficulty), metadata) { - OnlineID = beatmapSet.OnlineID ?? -1, - DateAdded = beatmapSet.DateAdded, - Status = beatmapSet.Status, - DeletePending = beatmapSet.DeletePending, - Hash = beatmapSet.Hash, - Protected = beatmapSet.Protected, + DifficultyName = beatmap.DifficultyName, + Status = beatmap.Status, + OnlineID = beatmap.OnlineID ?? -1, + Length = beatmap.Length, + BPM = beatmap.BPM, + Hash = beatmap.Hash, + StarRating = beatmap.StarRating, + MD5Hash = beatmap.MD5Hash, + Hidden = beatmap.Hidden, + AudioLeadIn = beatmap.AudioLeadIn, + StackLeniency = beatmap.StackLeniency, + SpecialStyle = beatmap.SpecialStyle, + LetterboxInBreaks = beatmap.LetterboxInBreaks, + WidescreenStoryboard = beatmap.WidescreenStoryboard, + EpilepsyWarning = beatmap.EpilepsyWarning, + SamplesMatchPlaybackRate = beatmap.SamplesMatchPlaybackRate, + DistanceSpacing = beatmap.DistanceSpacing, + BeatDivisor = beatmap.BeatDivisor, + GridSize = beatmap.GridSize, + TimelineZoom = beatmap.TimelineZoom, + Countdown = beatmap.Countdown, + CountdownOffset = beatmap.CountdownOffset, + MaxCombo = beatmap.MaxCombo, + Bookmarks = beatmap.Bookmarks, + BeatmapSet = realmBeatmapSet, }; - migrateFiles(beatmapSet, realm, realmBeatmapSet); - - foreach (var beatmap in beatmapSet.Beatmaps) - { - var ruleset = realm.Find(beatmap.RulesetInfo.ShortName); - var metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata); - - var realmBeatmap = new BeatmapInfo(ruleset, new BeatmapDifficulty(beatmap.BaseDifficulty), metadata) - { - DifficultyName = beatmap.DifficultyName, - Status = beatmap.Status, - OnlineID = beatmap.OnlineID ?? -1, - Length = beatmap.Length, - BPM = beatmap.BPM, - Hash = beatmap.Hash, - StarRating = beatmap.StarRating, - MD5Hash = beatmap.MD5Hash, - Hidden = beatmap.Hidden, - AudioLeadIn = beatmap.AudioLeadIn, - StackLeniency = beatmap.StackLeniency, - SpecialStyle = beatmap.SpecialStyle, - LetterboxInBreaks = beatmap.LetterboxInBreaks, - WidescreenStoryboard = beatmap.WidescreenStoryboard, - EpilepsyWarning = beatmap.EpilepsyWarning, - SamplesMatchPlaybackRate = beatmap.SamplesMatchPlaybackRate, - DistanceSpacing = beatmap.DistanceSpacing, - BeatDivisor = beatmap.BeatDivisor, - GridSize = beatmap.GridSize, - TimelineZoom = beatmap.TimelineZoom, - Countdown = beatmap.Countdown, - CountdownOffset = beatmap.CountdownOffset, - MaxCombo = beatmap.MaxCombo, - Bookmarks = beatmap.Bookmarks, - BeatmapSet = realmBeatmapSet, - }; - - realmBeatmapSet.Beatmaps.Add(realmBeatmap); - } - - realm.Add(realmBeatmapSet); + realmBeatmapSet.Beatmaps.Add(realmBeatmap); } - } - finally - { - transaction.Commit(); - } - log($"Successfully migrated {count} beatmaps to realm"); + r.Add(realmBeatmapSet); + } } + finally + { + transaction.Commit(); + } + + log($"Successfully migrated {count} beatmaps to realm"); }); } @@ -276,74 +280,66 @@ namespace osu.Game.Database int count = existingScores.Count(); - realmContextFactory.Run(realm => + realm.Run(r => { log($"Found {count} scores in EF"); - // only migrate data if the realm database is empty. - if (realm.All().Any()) - { - log("Skipping migration as realm already has scores loaded"); - } - else - { - var transaction = realm.BeginWrite(); - int written = 0; + var transaction = r.BeginWrite(); + int written = 0; - try + try + { + foreach (var score in existingScores) { - foreach (var score in existingScores) + if (++written % 1000 == 0) { - if (++written % 1000 == 0) - { - transaction.Commit(); - transaction = realm.BeginWrite(); - log($"Migrated {written}/{count} scores..."); - } - - var beatmap = realm.All().First(b => b.Hash == score.BeatmapInfo.Hash); - var ruleset = realm.Find(score.Ruleset.ShortName); - var user = new RealmUser - { - OnlineID = score.User.OnlineID, - Username = score.User.Username - }; - - var realmScore = new ScoreInfo(beatmap, ruleset, user) - { - Hash = score.Hash, - DeletePending = score.DeletePending, - OnlineID = score.OnlineID ?? -1, - ModsJson = score.ModsJson, - StatisticsJson = score.StatisticsJson, - TotalScore = score.TotalScore, - MaxCombo = score.MaxCombo, - Accuracy = score.Accuracy, - HasReplay = ((IScoreInfo)score).HasReplay, - Date = score.Date, - PP = score.PP, - Rank = score.Rank, - HitEvents = score.HitEvents, - Passed = score.Passed, - Combo = score.Combo, - Position = score.Position, - Statistics = score.Statistics, - Mods = score.Mods, - APIMods = score.APIMods, - }; - - migrateFiles(score, realm, realmScore); - - realm.Add(realmScore); + transaction.Commit(); + transaction = r.BeginWrite(); + log($"Migrated {written}/{count} scores..."); } - } - finally - { - transaction.Commit(); - } - log($"Successfully migrated {count} scores to realm"); + var beatmap = r.All().First(b => b.Hash == score.BeatmapInfo.Hash); + var ruleset = r.Find(score.Ruleset.ShortName); + var user = new RealmUser + { + OnlineID = score.User.OnlineID, + Username = score.User.Username + }; + + var realmScore = new ScoreInfo(beatmap, ruleset, user) + { + Hash = score.Hash, + DeletePending = score.DeletePending, + OnlineID = score.OnlineID ?? -1, + ModsJson = score.ModsJson, + StatisticsJson = score.StatisticsJson, + TotalScore = score.TotalScore, + MaxCombo = score.MaxCombo, + Accuracy = score.Accuracy, + HasReplay = ((IScoreInfo)score).HasReplay, + Date = score.Date, + PP = score.PP, + Rank = score.Rank, + HitEvents = score.HitEvents, + Passed = score.Passed, + Combo = score.Combo, + Position = score.Position, + Statistics = score.Statistics, + Mods = score.Mods, + APIMods = score.APIMods, + }; + + migrateFiles(score, r, realmScore); + + r.Add(realmScore); + } } + finally + { + transaction.Commit(); + } + + log($"Successfully migrated {count} scores to realm"); }); } @@ -373,13 +369,13 @@ namespace osu.Game.Database break; } - realmContextFactory.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { // only migrate data if the realm database is empty. - // note that this cannot be written as: `realm.All().All(s => s.Protected)`, because realm does not support `.All()`. - if (!realm.All().Any(s => !s.Protected)) + // note that this cannot be written as: `r.All().All(s => s.Protected)`, because realm does not support `.All()`. + if (!r.All().Any(s => !s.Protected)) { log($"Migrating {existingSkins.Count} skins"); @@ -394,9 +390,9 @@ namespace osu.Game.Database InstantiationInfo = skin.InstantiationInfo, }; - migrateFiles(skin, realm, realmSkin); + migrateFiles(skin, r, realmSkin); - realm.Add(realmSkin); + r.Add(realmSkin); if (skin.ID == userSkinInt) userSkinChoice.Value = realmSkin.ID.ToString(); @@ -432,12 +428,12 @@ namespace osu.Game.Database log("Beginning settings migration to realm"); - realmContextFactory.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { // only migrate data if the realm database is empty. - if (!realm.All().Any()) + if (!r.All().Any()) { log($"Migrating {existingSettings.Count} settings"); @@ -451,7 +447,7 @@ namespace osu.Game.Database if (string.IsNullOrEmpty(shortName)) continue; - realm.Add(new RealmRulesetSetting + r.Add(new RealmRulesetSetting { Key = dkb.Key, Value = dkb.StringValue, diff --git a/osu.Game/Database/EmptyRealmSet.cs b/osu.Game/Database/EmptyRealmSet.cs new file mode 100644 index 0000000000..b7f27ba035 --- /dev/null +++ b/osu.Game/Database/EmptyRealmSet.cs @@ -0,0 +1,46 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Realms; +using Realms.Schema; + +#nullable enable + +namespace osu.Game.Database +{ + public class EmptyRealmSet : IRealmCollection + { + private IList emptySet => Array.Empty(); + + public IEnumerator GetEnumerator() => emptySet.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator(); + public int Count => emptySet.Count; + public T this[int index] => emptySet[index]; + public int IndexOf(object item) => emptySet.IndexOf((T)item); + public bool Contains(object item) => emptySet.Contains((T)item); + + public event NotifyCollectionChangedEventHandler? CollectionChanged + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + public event PropertyChangedEventHandler? PropertyChanged + { + add => throw new NotImplementedException(); + remove => throw new NotImplementedException(); + } + + public IRealmCollection Freeze() => throw new NotImplementedException(); + public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback) => throw new NotImplementedException(); + public bool IsValid => throw new NotImplementedException(); + public Realm Realm => throw new NotImplementedException(); + public ObjectSchema ObjectSchema => throw new NotImplementedException(); + public bool IsFrozen => throw new NotImplementedException(); + } +} diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index d00cfb2035..3047a1d30a 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -45,7 +45,7 @@ namespace osu.Game.Database /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + ILive? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// A user displayable name for the model type associated with this manager. diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmAccess.cs similarity index 59% rename from osu.Game/Database/RealmContextFactory.cs rename to osu.Game/Database/RealmAccess.cs index ea6a4b9636..4fdfaa804c 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -28,9 +30,9 @@ using Realms.Exceptions; namespace osu.Game.Database { /// - /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. + /// A factory which provides safe access to the realm storage backend. /// - public class RealmContextFactory : IDisposable + public class RealmAccess : IDisposable { private readonly Storage storage; @@ -55,46 +57,75 @@ namespace osu.Game.Database private const int schema_version = 13; /// - /// Lock object which is held during sections, blocking context creation during blocking periods. + /// Lock object which is held during sections, blocking realm retrieval during blocking periods. /// - private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1); - private readonly ThreadLocal currentThreadCanCreateContexts = new ThreadLocal(); + private readonly ThreadLocal currentThreadCanCreateRealmInstances = new ThreadLocal(); - private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)"); + /// + /// Holds a map of functions registered via and and a coinciding action which when triggered, + /// will unregister the subscription from realm. + /// + /// Put another way, the key is an action which registers the subscription with realm. The returned from the action is stored as the value and only + /// used internally. + /// + /// Entries in this dictionary are only removed when a consumer signals that the subscription should be permanently ceased (via their own ). + /// + private readonly Dictionary, IDisposable?> customSubscriptionsResetMap = new Dictionary, IDisposable?>(); - private readonly object contextLock = new object(); + /// + /// Holds a map of functions registered via and a coinciding action which when triggered, + /// fires a change set event with an empty collection. This is used to inform subscribers when the main realm instance gets recycled, and ensure they don't use invalidated + /// managed realm objects from a previous firing. + /// + private readonly Dictionary, Action> notificationsResetMap = new Dictionary, Action>(); - private Realm? context; + private static readonly GlobalStatistic realm_instances_created = GlobalStatistics.Get(@"Realm", @"Instances (Created)"); - public Realm Context + private static readonly GlobalStatistic total_subscriptions = GlobalStatistics.Get(@"Realm", @"Subscriptions"); + + private readonly object realmLock = new object(); + + private Realm? updateRealm; + + public Realm Realm => ensureUpdateRealm(); + + private Realm ensureUpdateRealm() { - get + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"Use {nameof(getRealmInstance)} when performing realm operations from a non-update thread"); + + lock (realmLock) { - if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException(@$"Use {nameof(Run)}/{nameof(Write)} when performing realm operations from a non-update thread"); - - lock (contextLock) + if (updateRealm == null) { - if (context == null) - { - context = createContext(); - Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}"); - } + updateRealm = getRealmInstance(); - // creating a context will ensure our schema is up-to-date and migrated. - return context; + Logger.Log(@$"Opened realm ""{updateRealm.Config.DatabasePath}"" at version {updateRealm.Config.SchemaVersion}"); + + // Resubscribe any subscriptions + foreach (var action in customSubscriptionsResetMap.Keys) + registerSubscription(action); } + + Debug.Assert(updateRealm != null); + + return updateRealm; } } + internal static bool CurrentThreadSubscriptionsAllowed => current_thread_subscriptions_allowed.Value; + + private static readonly ThreadLocal current_thread_subscriptions_allowed = new ThreadLocal(); + /// - /// Construct a new instance of a realm context factory. + /// Construct a new instance. /// /// 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) + public RealmAccess(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) { this.storage = storage; this.efContextFactory = efContextFactory; @@ -108,7 +139,7 @@ namespace osu.Game.Database try { - // This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date. + // This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date. cleanupPendingDeletions(); } catch (Exception e) @@ -124,7 +155,7 @@ namespace osu.Game.Database private void cleanupPendingDeletions() { - using (var realm = createContext()) + using (var realm = getRealmInstance()) using (var transaction = realm.BeginWrite()) { var pendingDeleteScores = realm.All().Where(s => s.DeletePending); @@ -172,34 +203,28 @@ namespace osu.Game.Database /// /// Run work on realm with a return value. /// - /// - /// Handles correct context management automatically. - /// /// The work to run. /// The return type. public T Run(Func action) { if (ThreadSafety.IsUpdateThread) - return action(Context); + return action(Realm); - using (var realm = createContext()) + using (var realm = getRealmInstance()) return action(realm); } /// /// Run work on realm. /// - /// - /// Handles correct context management automatically. - /// /// The work to run. public void Run(Action action) { if (ThreadSafety.IsUpdateThread) - action(Context); + action(Realm); else { - using (var realm = createContext()) + using (var realm = getRealmInstance()) action(realm); } } @@ -207,44 +232,155 @@ namespace osu.Game.Database /// /// Write changes to realm. /// - /// - /// Handles correct context management and transaction committing automatically. - /// /// The work to run. public void Write(Action action) { if (ThreadSafety.IsUpdateThread) - Context.Write(action); + Realm.Write(action); else { - using (var realm = createContext()) + using (var realm = getRealmInstance()) realm.Write(action); } } - private Realm createContext() + /// + /// Subscribe to a realm collection and begin watching for asynchronous changes. + /// + /// + /// This adds osu! specific thread and managed state safety checks on top of . + /// + /// In addition to the documented realm behaviour, we have the additional requirement of handling subscriptions over potential realm instance recycle. + /// When this happens, callback events will be automatically fired: + /// - On recycle start, a callback with an empty collection and null will be invoked. + /// - On recycle end, a standard initial realm callback will arrive, with null and an up-to-date collection. + /// + /// The to observe for changes. + /// Type of the elements in the list. + /// The callback to be invoked with the updated . + /// + /// A subscription token. It must be kept alive for as long as you want to receive change notifications. + /// To stop receiving notifications, call . + /// + /// + public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate callback) + where T : RealmObjectBase + { + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); + + lock (realmLock) + { + Func action = realm => query(realm).QueryAsyncWithNotifications(callback); + + // Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing. + notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null, null)); + return RegisterCustomSubscription(action); + } + } + + /// + /// Run work on realm that will be run every time the update thread realm instance gets recycled. + /// + /// The work to run. Return value should be an from QueryAsyncWithNotifications, or an to clean up any bindings. + /// An which should be disposed to unsubscribe any inner subscription. + public IDisposable RegisterCustomSubscription(Func action) + { + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); + + var syncContext = SynchronizationContext.Current; + + total_subscriptions.Value++; + + registerSubscription(action); + + // This token is returned to the consumer. + // When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class). + return new InvokeOnDisposal(() => + { + if (ThreadSafety.IsUpdateThread) + unsubscribe(); + else + syncContext.Post(_ => unsubscribe(), null); + + void unsubscribe() + { + lock (realmLock) + { + if (customSubscriptionsResetMap.TryGetValue(action, out var unsubscriptionAction)) + { + unsubscriptionAction?.Dispose(); + customSubscriptionsResetMap.Remove(action); + notificationsResetMap.Remove(action); + total_subscriptions.Value--; + } + } + } + }); + } + + private void registerSubscription(Func action) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + lock (realmLock) + { + // Retrieve realm instance outside of flag update to ensure that the instance is retrieved, + // as attempting to access it inside the subscription if it's not constructed would lead to + // cyclic invocations of the subscription callback. + var realm = Realm; + + Debug.Assert(!customSubscriptionsResetMap.TryGetValue(action, out var found) || found == null); + + current_thread_subscriptions_allowed.Value = true; + customSubscriptionsResetMap[action] = action(realm); + current_thread_subscriptions_allowed.Value = false; + } + } + + /// + /// Unregister all subscriptions when the realm instance is to be recycled. + /// Subscriptions will still remain and will be re-subscribed when the realm instance returns. + /// + private void unregisterAllSubscriptions() + { + lock (realmLock) + { + foreach (var action in notificationsResetMap.Values) + action(); + + foreach (var action in customSubscriptionsResetMap) + { + action.Value?.Dispose(); + customSubscriptionsResetMap[action.Key] = null; + } + } + } + + private Realm getRealmInstance() { if (isDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); + throw new ObjectDisposedException(nameof(RealmAccess)); bool tookSemaphoreLock = false; try { - if (!currentThreadCanCreateContexts.Value) + if (!currentThreadCanCreateRealmInstances.Value) { - contextCreationLock.Wait(); - currentThreadCanCreateContexts.Value = true; + realmRetrievalLock.Wait(); + currentThreadCanCreateRealmInstances.Value = true; tookSemaphoreLock = true; } else { - // the semaphore is used to handle blocking of all context creation during certain periods. - // once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread. - // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`. + // the semaphore is used to handle blocking of all realm retrieval during certain periods. + // once the semaphore has been taken by this code section, it is safe to retrieve further realm instances on the same thread. + // this can happen if a realm subscription is active and triggers a callback which has user code that calls `Run`. } - contexts_created.Value++; + realm_instances_created.Value++; return Realm.GetInstance(getConfiguration()); } @@ -252,8 +388,8 @@ namespace osu.Game.Database { if (tookSemaphoreLock) { - contextCreationLock.Release(); - currentThreadCanCreateContexts.Value = false; + realmRetrievalLock.Release(); + currentThreadCanCreateRealmInstances.Value = false; } } } @@ -442,7 +578,7 @@ namespace osu.Game.Database } /// - /// Flush any active contexts and block any further writes. + /// Flush any active realm instances and block any further writes. /// /// /// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm. @@ -452,21 +588,36 @@ namespace osu.Game.Database public IDisposable BlockAllOperations() { if (isDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); + throw new ObjectDisposedException(nameof(RealmAccess)); + + SynchronizationContext? syncContext = null; try { - contextCreationLock.Wait(); + realmRetrievalLock.Wait(); - lock (contextLock) + lock (realmLock) { - if (!ThreadSafety.IsUpdateThread && context != null) - throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); + if (updateRealm == null) + { + // null realm means the update thread has not yet retrieved its instance. + // we don't need to worry about reviving the update instance in this case, so don't bother with the SynchronizationContext. + Debug.Assert(!ThreadSafety.IsUpdateThread); + } + else + { + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); + + syncContext = SynchronizationContext.Current; + } + + unregisterAllSubscriptions(); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - context?.Dispose(); - context = null; + updateRealm?.Dispose(); + updateRealm = null; } const int sleep_length = 200; @@ -493,15 +644,19 @@ namespace osu.Game.Database } catch { - contextCreationLock.Release(); + restoreOperation(); throw; } - return new InvokeOnDisposal(this, factory => + return new InvokeOnDisposal(restoreOperation); + + void restoreOperation() { - factory.contextCreationLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); - }); + realmRetrievalLock.Release(); + // Post back to the update thread to revive any subscriptions. + syncContext?.Post(_ => ensureUpdateRealm(), null); + } } // https://github.com/realm/realm-dotnet/blob/32f4ebcc88b3e80a3b254412665340cd9f3bd6b5/Realm/Realm/Extensions/ReflectionExtensions.cs#L46 @@ -511,16 +666,16 @@ namespace osu.Game.Database public void Dispose() { - lock (contextLock) + lock (realmLock) { - context?.Dispose(); + updateRealm?.Dispose(); } if (!isDisposed) { - // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. - contextCreationLock.Wait(); - contextCreationLock.Dispose(); + // intentionally block realm retrieval indefinitely. this ensures that nothing can start consuming a new instance after disposal. + realmRetrievalLock.Wait(); + realmRetrievalLock.Dispose(); isDisposed = true; } diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index df5e165f8e..f06ba1eff9 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -24,17 +24,17 @@ namespace osu.Game.Database /// private readonly T data; - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; /// /// Construct a new instance of live realm data. /// /// The realm data. - /// The realm factory the data was sourced from. May be null for an unmanaged object. - public RealmLive(T data, RealmContextFactory realmFactory) + /// The realm factory the data was sourced from. May be null for an unmanaged object. + public RealmLive(T data, RealmAccess realm) { this.data = data; - this.realmFactory = realmFactory; + this.realm = realm; ID = data.ID; } @@ -51,10 +51,7 @@ namespace osu.Game.Database return; } - realmFactory.Run(realm => - { - perform(retrieveFromID(realm, ID)); - }); + realm.Run(r => perform(retrieveFromID(r, ID))); } /// @@ -66,9 +63,9 @@ namespace osu.Game.Database if (!IsManaged) return perform(data); - return realmFactory.Run(realm => + return realm.Run(r => { - var returnData = perform(retrieveFromID(realm, ID)); + var returnData = perform(retrieveFromID(r, ID)); if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}."); @@ -104,7 +101,7 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads"); - return realmFactory.Context.Find(ID); + return realm.Realm.Find(ID); } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index c25aeab336..d4f8978ac5 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Runtime.Serialization; using AutoMapper; using AutoMapper.Internal; -using osu.Framework.Development; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Models; @@ -217,16 +216,16 @@ namespace osu.Game.Database return new RealmLiveUnmanaged(realmObject); } - public static List> ToLive(this IEnumerable realmList, RealmContextFactory realmContextFactory) + public static List> ToLive(this IEnumerable realmList, RealmAccess realm) where T : RealmObject, IHasGuidPrimaryKey { - return realmList.Select(l => new RealmLive(l, realmContextFactory)).Cast>().ToList(); + return realmList.Select(l => new RealmLive(l, realm)).Cast>().ToList(); } - public static ILive ToLive(this T realmObject, RealmContextFactory realmContextFactory) + public static ILive ToLive(this T realmObject, RealmAccess realm) where T : RealmObject, IHasGuidPrimaryKey { - return new RealmLive(realmObject, realmContextFactory); + return new RealmLive(realmObject, realm); } /// @@ -272,9 +271,8 @@ namespace osu.Game.Database public static IDisposable? QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback) where T : RealmObjectBase { - // Subscriptions can only work on the main thread. - if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException("Cannot subscribe for realm notifications from a non-update thread."); + if (!RealmAccess.CurrentThreadSubscriptionsAllowed) + throw new InvalidOperationException($"Make sure to call {nameof(RealmAccess)}.{nameof(RealmAccess.RegisterForNotifications)}"); return collection.SubscribeForNotifications(callback); } diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs index 950b5aae09..b381ac70b0 100644 --- a/osu.Game/IO/IStorageResourceProvider.cs +++ b/osu.Game/IO/IStorageResourceProvider.cs @@ -28,7 +28,7 @@ namespace osu.Game.IO /// /// Access realm. /// - RealmContextFactory RealmContextFactory { get; } + RealmAccess RealmAccess { get; } /// /// Create a texture loader store based on an underlying data store. diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 03b069d431..ba129b93e5 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 { @@ -23,10 +24,9 @@ namespace osu.Game.Input.Bindings private readonly int? variant; private IDisposable realmSubscription; - private IQueryable realmKeyBindings; [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); @@ -49,32 +49,26 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - string rulesetName = ruleset?.ShortName; - - realmKeyBindings = realmFactory.Context.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant); - - realmSubscription = realmKeyBindings - .QueryAsyncWithNotifications((sender, changes, error) => - { - // first subscription ignored as we are handling this in LoadComplete. - if (changes == null) - return; - - ReloadMappings(); - }); + realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, changes, error) => + { + // The first fire of this is a bit redundant as this is being called in base.LoadComplete, + // but this is safest in case the subscription is restored after a context recycle. + reloadMappings(sender.AsQueryable()); + }); base.LoadComplete(); } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); + protected override void ReloadMappings() => reloadMappings(queryRealmKeyBindings(realm.Realm)); - realmSubscription?.Dispose(); + private IQueryable queryRealmKeyBindings(Realm realm) + { + string rulesetName = ruleset?.ShortName; + return realm.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant); } - protected override void ReloadMappings() + private void reloadMappings(IQueryable realmKeyBindings) { var defaults = DefaultKeyBindings.ToList(); @@ -93,5 +87,12 @@ namespace osu.Game.Input.Bindings else KeyBindings = newBindings; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + realmSubscription?.Dispose(); + } } } diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 60f7eb2198..20971ffca5 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -16,12 +16,12 @@ namespace osu.Game.Input { public class RealmKeyBindingStore { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; private readonly ReadableKeyCombinationProvider keyCombinationProvider; - public RealmKeyBindingStore(RealmContextFactory realmFactory, ReadableKeyCombinationProvider keyCombinationProvider) + public RealmKeyBindingStore(RealmAccess realm, ReadableKeyCombinationProvider keyCombinationProvider) { - this.realmFactory = realmFactory; + this.realm = realm; this.keyCombinationProvider = keyCombinationProvider; } @@ -34,7 +34,7 @@ namespace osu.Game.Input { List combinations = new List(); - realmFactory.Run(context => + realm.Run(context => { foreach (var action in context.All().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction)) { @@ -56,21 +56,21 @@ namespace osu.Game.Input /// The rulesets to populate defaults from. public void Register(KeyBindingContainer container, IEnumerable rulesets) { - realmFactory.Run(realm => + realm.Run(r => { - using (var transaction = realm.BeginWrite()) + using (var transaction = r.BeginWrite()) { // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed. // this is much faster as a result. - var existingBindings = realm.All().ToList(); + var existingBindings = r.All().ToList(); - insertDefaults(realm, existingBindings, container.DefaultKeyBindings); + insertDefaults(r, existingBindings, container.DefaultKeyBindings); foreach (var ruleset in rulesets) { var instance = ruleset.CreateInstance(); foreach (int variant in instance.AvailableVariants) - insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant); + insertDefaults(r, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant); } transaction.Commit(); diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index be5bdea6f1..9f795f007a 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -22,7 +22,7 @@ namespace osu.Game.Online private IDisposable? realmSubscription; [Resolved] - private RealmContextFactory realmContextFactory { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; public BeatmapDownloadTracker(IBeatmapSetInfo trackedItem) : base(trackedItem) @@ -42,7 +42,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realmContextFactory.Context.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 1f77b1d383..c67cbade6a 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -30,7 +30,7 @@ namespace osu.Game.Online.Rooms protected override bool RequiresChildrenUpdate => true; [Resolved] - private RealmContextFactory realmContextFactory { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; /// /// The availability state of the currently selected playlist item. @@ -78,7 +78,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = filteredBeatmaps().QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(), (items, changes, ___) => { if (changes == null) return; @@ -128,9 +128,9 @@ namespace osu.Game.Online.Rooms int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - return realmContextFactory.Context - .All() - .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); + return realm.Realm + .All() + .Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index b34586567d..d7e31c8a59 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -23,7 +23,7 @@ namespace osu.Game.Online private IDisposable? realmSubscription; [Resolved] - private RealmContextFactory realmContextFactory { get; set; } = null!; + private RealmAccess realm { get; set; } = null!; public ScoreDownloadTracker(ScoreInfo trackedItem) : base(trackedItem) @@ -47,7 +47,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; - realmSubscription = realmContextFactory.Context.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 710a7be8d4..09ca4e450d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -149,7 +149,7 @@ namespace osu.Game private MultiplayerClient multiplayerClient; - private RealmContextFactory realmFactory; + private RealmAccess realm; protected override Container Content => content; @@ -192,9 +192,9 @@ namespace osu.Game if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME)) dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", EFContextFactory)); + dependencies.Cache(realm = new RealmAccess(Storage, "client", EFContextFactory)); - dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage)); + dependencies.Cache(RulesetStore = new RulesetStore(realm, Storage)); dependencies.CacheAs(RulesetStore); // Backup is taken here rather than in EFToRealmMigrator to avoid recycling realm contexts @@ -205,11 +205,16 @@ namespace osu.Game string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; EFContextFactory.CreateBackup($"client.{migration}.db"); - realmFactory.CreateBackup($"client.{migration}.realm"); + realm.CreateBackup($"client.{migration}.realm"); using (var source = Storage.GetStream("collection.db")) - using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew)) - source.CopyTo(destination); + { + if (source != null) + { + using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew)) + source.CopyTo(destination); + } + } } dependencies.CacheAs(Storage); @@ -225,7 +230,7 @@ namespace osu.Game Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; - dependencies.Cache(SkinManager = new SkinManager(Storage, realmFactory, Host, Resources, Audio, Scheduler)); + dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler)); dependencies.CacheAs(SkinManager); EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); @@ -240,8 +245,8 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realmFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realmFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, Host, () => difficultyCache, LocalConfig)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API)); @@ -259,7 +264,7 @@ namespace osu.Game dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); - dependencies.CacheAs(rulesetConfigCache = new RulesetConfigCache(realmFactory, RulesetStore)); + dependencies.CacheAs(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore)); var powerStatus = CreateBatteryInfo(); if (powerStatus != null) @@ -303,7 +308,7 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); - KeyBindingStore = new RealmKeyBindingStore(realmFactory, keyCombinationProvider); + KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); dependencies.Cache(globalBindings); @@ -405,7 +410,7 @@ namespace osu.Game Scheduler.Add(() => { - realmBlocker = realmFactory.BlockAllOperations(); + realmBlocker = realm.BlockAllOperations(); readyToRun.Set(); }, false); @@ -483,7 +488,7 @@ namespace osu.Game BeatmapManager?.Dispose(); LocalConfig?.Dispose(); - realmFactory?.Dispose(); + realm?.Dispose(); } } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 70f8332295..9dc55d24dd 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -30,16 +30,7 @@ namespace osu.Game.Overlays [Resolved] private BeatmapManager beatmaps { get; set; } - public IBindableList BeatmapSets - { - get - { - if (LoadState < LoadState.Ready) - throw new InvalidOperationException($"{nameof(BeatmapSets)} should not be accessed before the music controller is loaded."); - - return beatmapSets; - } - } + public IBindableList BeatmapSets => beatmapSets; /// /// Point in time after which the current track will be restarted on triggering a "previous track" action. @@ -69,7 +60,7 @@ namespace osu.Game.Overlays public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000)); [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } [BackgroundDependencyLoader] private void load() @@ -80,26 +71,26 @@ namespace osu.Game.Overlays mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } + private IQueryable queryRealmBeatmapSets() => + realm.Realm + .All() + .Where(s => !s.DeletePending); + protected override void LoadComplete() { base.LoadComplete(); - - var availableBeatmaps = realmFactory.Context - .All() - .Where(s => !s.DeletePending); - - // ensure we're ready before completing async load. - // probably not a good way of handling this (as there is a period we aren't watching for changes until the realm subscription finishes up. - foreach (var s in availableBeatmaps) - beatmapSets.Add(s); - - beatmapSubscription = availableBeatmaps.QueryAsyncWithNotifications(beatmapsChanged); + beatmapSubscription = realm.RegisterForNotifications(r => queryRealmBeatmapSets(), beatmapsChanged); } private void beatmapsChanged(IRealmCollection sender, ChangeSet changes, Exception error) { if (changes == null) + { + beatmapSets.Clear(); + foreach (var s in sender) + beatmapSets.Add(s.Detach()); return; + } foreach (int i in changes.InsertedIndices) beatmapSets.Insert(i, sender[i].Detach()); diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 8809dec642..e4e3931048 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Localisation; @@ -118,6 +119,8 @@ namespace osu.Game.Overlays { ++runningDepth; + Logger.Log($"⚠️ {notification.Text}"); + notification.Closed += notificationClosed; if (notification is IHasCompletionTarget hasCompletionTarget) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 44203e8ee7..ec6e9e09b3 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; @@ -25,6 +26,8 @@ namespace osu.Game.Overlays.Notifications /// public event Action Closed; + public abstract LocalisableString Text { get; set; } + /// /// Whether this notification should forcefully display itself. /// diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 5b74bff817..4735fcb7c1 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Notifications private LocalisableString text; - public LocalisableString Text + public override LocalisableString Text { get => text; set diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index c32e40ffc8..b9a1cc6d90 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Notifications { private LocalisableString text; - public LocalisableString Text + public override LocalisableString Text { get => text; set diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs index 8d4fc5fc9f..3b94cae171 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs @@ -1,9 +1,13 @@ // 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 System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Localisation; @@ -15,8 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings protected override LocalisableString Header => DebugSettingsStrings.MemoryHeader; [BackgroundDependencyLoader] - private void load(GameHost host, RealmContextFactory realmFactory) + private void load(GameHost host, RealmAccess realm) { + SettingsButton blockAction; + SettingsButton unblockAction; + Children = new Drawable[] { new SettingsButton @@ -30,11 +37,56 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings Action = () => { // Blocking operations implicitly causes a Compact(). - using (realmFactory.BlockAllOperations()) + using (realm.BlockAllOperations()) { } } }, + blockAction = new SettingsButton + { + Text = "Block realm", + }, + unblockAction = new SettingsButton + { + Text = "Unblock realm", + }, + }; + + blockAction.Action = () => + { + try + { + var token = realm.BlockAllOperations(); + + blockAction.Enabled.Value = false; + + // As a safety measure, unblock after 10 seconds. + // This is to handle the case where a dev may block, but then something on the update thread + // accesses realm and blocks for eternity. + Task.Factory.StartNew(() => + { + Thread.Sleep(10000); + unblock(); + }); + + unblockAction.Action = unblock; + + void unblock() + { + token?.Dispose(); + token = null; + + Scheduler.Add(() => + { + blockAction.Enabled.Value = true; + unblockAction.Action = null; + }); + } + } + catch (Exception e) + { + Logger.Error(e, "Blocking realm failed"); + } }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 60aff91301..2405618917 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -386,10 +386,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateStoreFromButton(KeyButton button) { - realmFactory.Run(realm => + realm.Run(r => { - var binding = realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); - realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); + var binding = r.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + r.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); }); } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 5b8a52240e..922d371261 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -30,13 +30,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input } [BackgroundDependencyLoader] - private void load(RealmContextFactory realmFactory) + private void load(RealmAccess realm) { string rulesetName = Ruleset?.ShortName; - var bindings = realmFactory.Run(realm => realm.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant) - .Detach()); + var bindings = realm.Run(r => r.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant) + .Detach()); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 0fa6d78d4b..8ab296c0a8 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -47,10 +47,15 @@ namespace osu.Game.Overlays.Settings.Sections private SkinManager skins { get; set; } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } private IDisposable realmSubscription; - private IQueryable realmSkins; + + private IQueryable queryRealmSkins() => + realm.Realm.All() + .Where(s => !s.DeletePending) + .OrderByDescending(s => s.Protected) // protected skins should be at the top. + .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor) @@ -78,20 +83,12 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Current = dropdownBindable; - realmSkins = realmFactory.Context.All() - .Where(s => !s.DeletePending) - .OrderByDescending(s => s.Protected) // protected skins should be at the top. - .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase); - - realmSubscription = realmSkins - .QueryAsyncWithNotifications((sender, changes, error) => - { - if (changes == null) - return; - - // Eventually this should be handling the individual changes rather than refreshing the whole dropdown. - updateItems(); - }); + realmSubscription = realm.RegisterForNotifications(r => queryRealmSkins(), (sender, changes, error) => + { + // The first fire of this is a bit redundant due to the call below, + // but this is safest in case the subscription is restored after a context recycle. + updateItems(); + }); updateItems(); @@ -131,9 +128,9 @@ namespace osu.Game.Overlays.Settings.Sections private void updateItems() { - int protectedCount = realmSkins.Count(s => s.Protected); + int protectedCount = queryRealmSkins().Count(s => s.Protected); - skinItems = realmSkins.ToLive(realmFactory); + skinItems = queryRealmSkins().ToLive(realm); skinItems.Insert(protectedCount, random_skin_info); diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 75bebfa763..c855b76680 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } protected ToolbarButton() : base(HoverSampleSet.Toolbar) @@ -207,7 +207,7 @@ namespace osu.Game.Overlays.Toolbar { if (Hotkey == null) return; - var realmKeyBinding = realmFactory.Context.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value); + var realmKeyBinding = realm.Realm.All().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value); if (realmKeyBinding != null) { diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index 60a6b70221..30bb95ba72 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Configuration public abstract class RulesetConfigManager : ConfigManager, IRulesetConfigManager where TLookup : struct, Enum { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; private readonly int variant; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Configuration protected RulesetConfigManager(SettingsStore store, RulesetInfo ruleset, int? variant = null) { - realmFactory = store?.Realm; + realm = store?.Realm; rulesetName = ruleset.ShortName; @@ -37,10 +37,10 @@ namespace osu.Game.Rulesets.Configuration protected override void PerformLoad() { - if (realmFactory != null) + if (realm != null) { // As long as RulesetConfigCache exists, there is no need to subscribe to realm events. - databasedSettings = realmFactory.Context.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).ToList(); + databasedSettings = realm.Realm.All().Where(b => b.RulesetName == rulesetName && b.Variant == variant).ToList(); } } @@ -56,11 +56,11 @@ namespace osu.Game.Rulesets.Configuration pendingWrites.Clear(); } - realmFactory?.Write(realm => + realm?.Write(r => { foreach (var c in changed) { - var setting = realm.All().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); + var setting = r.All().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); setting.Value = ConfigStore[c].ToString(); } @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Configuration Variant = variant, }; - realmFactory?.Context.Write(() => realmFactory.Context.Add(setting)); + realm?.Realm.Write(() => realm.Realm.Add(setting)); databasedSettings.Add(setting); } diff --git a/osu.Game/Rulesets/Mods/Metronome.cs b/osu.Game/Rulesets/Mods/Metronome.cs index 8b6d86c45f..b85a341577 100644 --- a/osu.Game/Rulesets/Mods/Metronome.cs +++ b/osu.Game/Rulesets/Mods/Metronome.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mods if (!IsBeatSyncedWithTrack) return; - int timeSignature = (int)timingPoint.TimeSignature; + int timeSignature = timingPoint.TimeSignature.Numerator; // play metronome from one measure before the first object. if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index a77a83b36c..e6487c6b29 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.Timing; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.OpenGL.Vertices; using osu.Game.Rulesets.Objects; @@ -32,9 +33,17 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override string Description => "Restricted view area."; - internal ModFlashlight() - { - } + [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] + public abstract BindableNumber SizeMultiplier { get; } + + [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")] + public abstract BindableBool ComboBasedSize { get; } + + /// + /// The default size of the flashlight in ruleset-appropriate dimensions. + /// and will apply their adjustments on top of this size. + /// + public abstract float DefaultFlashlightSize { get; } } public abstract class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor @@ -79,7 +88,7 @@ namespace osu.Game.Rulesets.Mods flashlight.Breaks = drawableRuleset.Beatmap.Breaks; } - public abstract Flashlight CreateFlashlight(); + protected abstract Flashlight CreateFlashlight(); public abstract class Flashlight : Drawable { @@ -93,6 +102,17 @@ namespace osu.Game.Rulesets.Mods public List Breaks; + private readonly float defaultFlashlightSize; + private readonly float sizeMultiplier; + private readonly bool comboBasedSize; + + protected Flashlight(ModFlashlight modFlashlight) + { + defaultFlashlightSize = modFlashlight.DefaultFlashlightSize; + sizeMultiplier = modFlashlight.SizeMultiplier.Value; + comboBasedSize = modFlashlight.ComboBasedSize.Value; + } + [BackgroundDependencyLoader] private void load(ShaderManager shaderManager) { @@ -124,6 +144,21 @@ namespace osu.Game.Rulesets.Mods protected abstract string FragmentShader { get; } + protected float GetSizeFor(int combo) + { + float size = defaultFlashlightSize * sizeMultiplier; + + if (comboBasedSize) + { + if (combo > 200) + size *= 0.8f; + else if (combo > 100) + size *= 0.9f; + } + + return size; + } + private Vector2 flashlightPosition; protected Vector2 FlashlightPosition diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index a44967c21c..993efead33 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mods { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - int beatsPerBar = (int)timingPoint.TimeSignature; + int beatsPerBar = timingPoint.TimeSignature.Numerator; int segmentLength = beatsPerBar * Divisor * bars_per_segment; if (!IsBeatSyncedWithTrack) @@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mods playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature); } - private void playBeatFor(int beatIndex, TimeSignatures signature) + private void playBeatFor(int beatIndex, TimeSignature signature) { if (beatIndex == 0) finishSample?.Play(); - switch (signature) + switch (signature.Numerator) { - case TimeSignatures.SimpleTriple: + case 3: switch (beatIndex % 6) { case 0: @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Mods break; - case TimeSignatures.SimpleQuadruple: + case 4: switch (beatIndex % 4) { case 0: diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs index e78aa5a5a0..d71a499119 100644 --- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs +++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs @@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Objects int currentBeat = 0; // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object - double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature; + double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator; - double barLength = currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature; + double barLength = currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator; for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++) { @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Objects BarLines.Add(new TBarLine { StartTime = t, - Major = currentBeat % (int)currentTimingPoint.TimeSignature == 0 + Major = currentBeat % currentTimingPoint.TimeSignature.Numerator == 0 }); } } diff --git a/osu.Game/Rulesets/RulesetConfigCache.cs b/osu.Game/Rulesets/RulesetConfigCache.cs index dee13e74a5..c4f1933cd8 100644 --- a/osu.Game/Rulesets/RulesetConfigCache.cs +++ b/osu.Game/Rulesets/RulesetConfigCache.cs @@ -13,14 +13,14 @@ namespace osu.Game.Rulesets { public class RulesetConfigCache : Component, IRulesetConfigCache { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; private readonly RulesetStore rulesets; private readonly Dictionary configCache = new Dictionary(); - public RulesetConfigCache(RealmContextFactory realmFactory, RulesetStore rulesets) + public RulesetConfigCache(RealmAccess realm, RulesetStore rulesets) { - this.realmFactory = realmFactory; + this.realm = realm; this.rulesets = rulesets; } @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets { base.LoadComplete(); - var settingsStore = new SettingsStore(realmFactory); + var settingsStore = new SettingsStore(realm); // let's keep things simple for now and just retrieve all the required configs at startup.. foreach (var ruleset in rulesets.AvailableRulesets) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index a9e5ff797c..9af9ace7ad 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets { public class RulesetStore : IDisposable, IRulesetStore { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realmAccess; private const string ruleset_library_prefix = @"osu.Game.Rulesets"; @@ -31,9 +31,9 @@ namespace osu.Game.Rulesets private readonly List availableRulesets = new List(); - public RulesetStore(RealmContextFactory realmFactory, Storage? storage = null) + public RulesetStore(RealmAccess realm, Storage? storage = null) { - this.realmFactory = realmFactory; + realmAccess = realm; // On android in release configuration assemblies are loaded from the apk directly into memory. // We cannot read assemblies from cwd, so should check loaded assemblies instead. @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets private void addMissingRulesets() { - realmFactory.Write(realm => + realmAccess.Write(realm => { var rulesets = realm.All(); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index f895134f97..3a842a048a 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -25,21 +25,21 @@ namespace osu.Game.Scoring { public class ScoreManager : IModelManager, IModelImporter { - private readonly RealmContextFactory contextFactory; + private readonly RealmAccess realm; private readonly Scheduler scheduler; private readonly Func difficulties; private readonly OsuConfigManager configManager; private readonly ScoreModelManager scoreModelManager; - public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory, Scheduler scheduler, + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) { - this.contextFactory = contextFactory; + this.realm = realm; this.scheduler = scheduler; this.difficulties = difficulties; this.configManager = configManager; - scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory); + scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, realm); } public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score); @@ -51,7 +51,7 @@ namespace osu.Game.Scoring /// The first result for the provided query, or null if no results were found. public ScoreInfo Query(Expression> query) { - return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.Detach()); + return realm.Run(r => r.All().FirstOrDefault(query)?.Detach()); } /// @@ -254,10 +254,10 @@ namespace osu.Game.Scoring public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { - contextFactory.Run(realm => + realm.Run(r => { - var items = realm.All() - .Where(s => !s.DeletePending); + var items = r.All() + .Where(s => !s.DeletePending); if (filter != null) items = items.Where(filter); @@ -308,7 +308,7 @@ namespace osu.Game.Scoring return scoreModelManager.Import(archive, lowPriority, cancellationToken); } - public Task> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public ILive Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); } diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs index 5e560effa1..59102360f9 100644 --- a/osu.Game/Scoring/ScoreModelManager.cs +++ b/osu.Game/Scoring/ScoreModelManager.cs @@ -29,8 +29,8 @@ namespace osu.Game.Scoring private readonly RulesetStore rulesets; private readonly Func beatmaps; - public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmContextFactory contextFactory) - : base(storage, contextFactory) + public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm) + : base(storage, realm) { this.rulesets = rulesets; this.beatmaps = beatmaps; @@ -74,7 +74,7 @@ namespace osu.Game.Scoring public override bool IsAvailableLocally(ScoreInfo model) { - return ContextFactory.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 1415014e59..cc4041394d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (beat == 0 && i == 0) nextMinTick = float.MinValue; - int indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value); + int indexInBar = beat % (point.TimeSignature.Numerator * beatDivisor.Value); int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); var colour = BindableBeatDivisor.GetColourFor(divisor, colours); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8c4b458534..b42f629aad 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -158,9 +158,6 @@ namespace osu.Game.Screens.Edit return; } - beatDivisor.Value = playableBeatmap.BeatmapInfo.BeatDivisor; - beatDivisor.BindValueChanged(divisor => playableBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); - // Todo: should probably be done at a DrawableRuleset level to share logic with Player. clock = new EditorClock(playableBeatmap, beatDivisor) { IsCoupled = false }; clock.ChangeSource(loadableBeatmap.Track); @@ -178,6 +175,9 @@ namespace osu.Game.Screens.Edit changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); + beatDivisor.Value = editorBeatmap.BeatmapInfo.BeatDivisor; + beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); + updateLastSavedHash(); Schedule(() => diff --git a/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs new file mode 100644 index 0000000000..51b58bd3dc --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs @@ -0,0 +1,97 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Timing +{ + public class LabelledTimeSignature : LabelledComponent + { + public LabelledTimeSignature() + : base(false) + { + } + + protected override TimeSignatureBox CreateComponent() => new TimeSignatureBox(); + + public class TimeSignatureBox : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(TimeSignature.SimpleQuadruple); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private OsuNumberBox numeratorBox; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + numeratorBox = new OsuNumberBox + { + Width = 40, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + CornerRadius = CORNER_RADIUS, + CommitOnFocusLost = true + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding + { + Left = 5, + Right = CONTENT_PADDING_HORIZONTAL + }, + Text = "/ 4", + Font = OsuFont.Default.With(size: 20) + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateFromCurrent(), true); + numeratorBox.OnCommit += (_, __) => updateFromNumeratorBox(); + } + + private void updateFromCurrent() + { + numeratorBox.Current.Value = Current.Value.Numerator.ToString(); + } + + private void updateFromNumeratorBox() + { + if (int.TryParse(numeratorBox.Current.Value, out int numerator) && numerator > 0) + Current.Value = new TimeSignature(numerator); + else + { + // trigger `Current` change to restore the numerator box's text to a valid value. + Current.TriggerChange(); + } + } + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs index ab840e56a7..f8ec4aef25 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes public class TimingRowAttribute : RowAttribute { private readonly BindableNumber beatLength; - private readonly Bindable timeSignature; + private readonly Bindable timeSignature; private OsuSpriteText text; public TimingRowAttribute(TimingControlPoint timing) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index a0bb9ac506..cd0b56d338 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Timing; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays.Settings; @@ -15,7 +14,7 @@ namespace osu.Game.Screens.Edit.Timing internal class TimingSection : Section { private SettingsSlider bpmSlider; - private SettingsEnumDropdown timeSignature; + private LabelledTimeSignature timeSignature; private BPMTextBox bpmTextEntry; [BackgroundDependencyLoader] @@ -25,10 +24,10 @@ namespace osu.Game.Screens.Edit.Timing { bpmTextEntry = new BPMTextBox(), bpmSlider = new BPMSlider(), - timeSignature = new SettingsEnumDropdown + timeSignature = new LabelledTimeSignature { - LabelText = "Time Signature" - }, + Label = "Time Signature" + } }); } diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 8c4a13f2bd..a72ba89dfa 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -74,11 +74,22 @@ namespace osu.Game.Screens base.OnEntering(last); LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal); - LoadComponentAsync(loadableScreen = CreateLoadableScreen()); // A non-null context factory means there's still content to migrate. if (efContextFactory != null) + { LoadComponentAsync(realmMigrator = new EFToRealmMigrator(), AddInternal); + realmMigrator.MigrationCompleted.ContinueWith(_ => Schedule(() => + { + // Delay initial screen loading to ensure that the migration is in a complete and sane state + // before the intro screen may import the game intro beatmap. + LoadComponentAsync(loadableScreen = CreateLoadableScreen()); + })); + } + else + { + LoadComponentAsync(loadableScreen = CreateLoadableScreen()); + } LoadComponentAsync(spinner = new LoadingSpinner(true, true) { @@ -96,7 +107,7 @@ namespace osu.Game.Screens private void checkIfLoaded() { - if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling || realmMigrator?.FinishedMigrating == false) + if (loadableScreen?.LoadState != LoadState.Ready || !precompiler.FinishedCompiling) { Schedule(checkIfLoaded); return; diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index e66ecc74e1..fceb083916 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Menu private BeatmapManager beatmaps { get; set; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, Framework.Game game, RealmContextFactory realmContextFactory) + private void load(OsuConfigManager config, Framework.Game game, RealmAccess realm) { // prevent user from changing beatmap while the intro is still running. beatmap = Beatmap.BeginLease(false); @@ -97,9 +97,9 @@ namespace osu.Game.Screens.Menu // if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection. if (!MenuMusic.Value) { - realmContextFactory.Run(realm => + realm.Run(r => { - var usableBeatmapSets = realm.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); + var usableBeatmapSets = r.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); int setCount = usableBeatmapSets.Count; diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index bdcd3020f8..cd0c75c1a1 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -94,9 +94,9 @@ namespace osu.Game.Screens.Menu if (beatIndex < 0) return; - if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % (int)timingPoint.TimeSignature == 0) + if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % timingPoint.TimeSignature.Numerator == 0) flash(leftBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes); - if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % (int)timingPoint.TimeSignature == 0) + if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % timingPoint.TimeSignature.Numerator == 0) flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes); } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f9388097ac..c82efe2d32 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -282,7 +282,7 @@ namespace osu.Game.Screens.Menu { this.Delay(early_activation).Schedule(() => { - if (beatIndex % (int)timingPoint.TimeSignature == 0) + if (beatIndex % timingPoint.TimeSignature.Numerator == 0) sampleDownbeat.Play(); else sampleBeat.Play(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4d3201cd27..cfca2d0a3d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1024,11 +1024,11 @@ namespace osu.Game.Screens.Play /// /// The to import. /// The imported score. - protected virtual async Task ImportScore(Score score) + protected virtual Task ImportScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) - return; + return Task.CompletedTask; LegacyByteArrayReader replayReader; @@ -1048,7 +1048,7 @@ namespace osu.Game.Screens.Play // conflicts across various systems (ie. solo and multiplayer). importableScore.OnlineID = -1; - var imported = await scoreManager.Import(importableScore, replayReader).ConfigureAwait(false); + var imported = scoreManager.Import(importableScore, replayReader); imported.PerformRead(s => { @@ -1056,6 +1056,8 @@ namespace osu.Game.Screens.Play score.ScoreInfo.Hash = s.Hash; score.ScoreInfo.ID = s.ID; }); + + return Task.CompletedTask; } /// diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f8cee2704b..dff2c598c3 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -179,24 +179,24 @@ namespace osu.Game.Screens.Select if (!loadedTestBeatmaps) { - realmFactory.Run(realm => loadBeatmapSets(getBeatmapSets(realm))); + realm.Run(r => loadBeatmapSets(getBeatmapSets(r))); } } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } protected override void LoadComplete() { base.LoadComplete(); - subscriptionSets = getBeatmapSets(realmFactory.Context).QueryAsyncWithNotifications(beatmapSetsChanged); - subscriptionBeatmaps = realmFactory.Context.All().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); + subscriptionSets = realm.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged); + subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => !b.Hidden), beatmapsChanged); // Can't use main subscriptions because we can't lookup deleted indices. // https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595. - subscriptionDeletedSets = realmFactory.Context.All().Where(s => s.DeletePending && !s.Protected).QueryAsyncWithNotifications(deletedBeatmapSetsChanged); - subscriptionHiddenBeatmaps = realmFactory.Context.All().Where(b => b.Hidden).QueryAsyncWithNotifications(beatmapsChanged); + subscriptionDeletedSets = realm.RegisterForNotifications(r => r.All().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged); + subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All().Where(b => b.Hidden), beatmapsChanged); } private void deletedBeatmapSetsChanged(IRealmCollection sender, ChangeSet changes, Exception error) @@ -231,7 +231,7 @@ namespace osu.Game.Screens.Select foreach (var id in realmSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) - UpdateBeatmapSet(realmFactory.Context.Find(id).Detach()); + UpdateBeatmapSet(realm.Realm.Find(id).Detach()); } foreach (var id in root.BeatmapSetsByID.Keys) @@ -274,7 +274,7 @@ namespace osu.Game.Screens.Select } } - private IRealmCollection getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection(); + private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => removeBeatmapSet(beatmapSet.ID); @@ -552,10 +552,11 @@ namespace osu.Game.Screens.Select private void signalBeatmapsLoaded() { - Debug.Assert(BeatmapSetsLoaded == false); - - BeatmapSetsChanged?.Invoke(); - BeatmapSetsLoaded = true; + if (!BeatmapSetsLoaded) + { + BeatmapSetsChanged?.Invoke(); + BeatmapSetsLoaded = true; + } itemsCache.Invalidate(); } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 7ac99f4935..e1f9c1b508 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select.Carousel private IBindable ruleset { get; set; } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } [Resolved] private IAPIProvider api { get; set; } @@ -48,18 +48,19 @@ namespace osu.Game.Screens.Select.Carousel ruleset.BindValueChanged(_ => { scoreSubscription?.Dispose(); - scoreSubscription = realmFactory.Context.All() - .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" - + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" - + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" - + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) - .OrderByDescending(s => s.TotalScore) - .QueryAsyncWithNotifications((items, changes, ___) => - { - Rank = items.FirstOrDefault()?.Rank; - // Required since presence is changed via IsPresent override - Invalidate(Invalidation.Presence); - }); + scoreSubscription = realm.RegisterForNotifications(r => + r.All() + .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) + .OrderByDescending(s => s.TotalScore), + (items, changes, ___) => + { + Rank = items.FirstOrDefault()?.Rank; + // Required since presence is changed via IsPresent override + Invalidate(Invalidation.Presence); + }); }, true); } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index da52b43ab6..f25997650b 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select.Leaderboards private RulesetStore rulesets { get; set; } [Resolved] - private RealmContextFactory realmFactory { get; set; } + private RealmAccess realm { get; set; } private BeatmapInfo beatmapInfo; @@ -44,9 +44,13 @@ namespace osu.Game.Screens.Select.Leaderboards beatmapInfo = value; Scores = null; - UpdateScores(); - if (IsLoaded) - refreshRealmSubscription(); + if (IsOnlineScope) + UpdateScores(); + else + { + if (IsLoaded) + refreshRealmSubscription(); + } } } @@ -109,15 +113,14 @@ namespace osu.Game.Screens.Select.Leaderboards if (beatmapInfo == null) return; - scoreSubscription = realmFactory.Context.All() - .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID) - .QueryAsyncWithNotifications((_, changes, ___) => - { - if (changes == null) - return; - - RefreshScores(); - }); + scoreSubscription = realm.RegisterForNotifications(r => + r.All() + .Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID), + (_, changes, ___) => + { + if (!IsOnlineScope) + RefreshScores(); + }); } protected override void Reset() @@ -147,12 +150,12 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { - realmFactory.Run(realm => + realm.Run(r => { - var scores = realm.All() - .AsEnumerable() - // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). - .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); + var scores = r.All() + .AsEnumerable() + // TODO: update to use a realm filter directly (or at least figure out the beatmap part to reduce scope). + .Where(s => !s.DeletePending && s.BeatmapInfo.ID == fetchBeatmapInfo.ID && s.Ruleset.OnlineID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index dd586bdd37..3cf9f79611 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -17,6 +17,7 @@ using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Scoring; +using Realms; namespace osu.Game.Screens.Spectate { @@ -56,7 +57,7 @@ namespace osu.Game.Screens.Spectate } [Resolved] - private RealmContextFactory realmContextFactory { get; set; } + private RealmAccess realm { get; set; } private IDisposable realmSubscription; @@ -79,23 +80,21 @@ namespace osu.Game.Screens.Spectate playingUserStates.BindTo(spectatorClient.PlayingUserStates); playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true); - realmSubscription = realmContextFactory.Context - .All() - .Where(s => !s.DeletePending) - .QueryAsyncWithNotifications((items, changes, ___) => - { - if (changes?.InsertedIndices == null) - return; - - foreach (int c in changes.InsertedIndices) - beatmapUpdated(items[c]); - }); + realmSubscription = realm.RegisterForNotifications( + realm => realm.All().Where(s => !s.DeletePending), beatmapsChanged); foreach ((int id, var _) in userMap) spectatorClient.WatchUser(id); })); } + private void beatmapsChanged(IRealmCollection items, ChangeSet changes, Exception ___) + { + if (changes?.InsertedIndices == null) return; + + foreach (int c in changes.InsertedIndices) beatmapUpdated(items[c]); + } + private void beatmapUpdated(BeatmapSetInfo beatmapSet) { foreach ((int userId, _) in userMap) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index d606d94b97..3685a26e26 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -43,8 +43,8 @@ namespace osu.Game.Skinning protected Skin(SkinInfo skin, IStorageResourceProvider resources, [CanBeNull] Stream configurationStream = null) { - SkinInfo = resources?.RealmContextFactory != null - ? skin.ToLive(resources.RealmContextFactory) + SkinInfo = resources?.RealmAccess != null + ? skin.ToLive(resources.RealmAccess) // This path should only be used in some tests. : skin.ToLiveUnmanaged(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 82bcd3b292..47c7bc060a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -11,7 +11,6 @@ using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; @@ -54,7 +53,7 @@ namespace osu.Game.Skinning }; private readonly SkinModelManager skinModelManager; - private readonly RealmContextFactory contextFactory; + private readonly RealmAccess realm; private readonly IResourceStore userFiles; @@ -68,9 +67,9 @@ namespace osu.Game.Skinning /// public Skin DefaultLegacySkin { get; } - public SkinManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IResourceStore resources, AudioManager audio, Scheduler scheduler) + public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore resources, AudioManager audio, Scheduler scheduler) { - this.contextFactory = contextFactory; + this.realm = realm; this.audio = audio; this.scheduler = scheduler; this.host = host; @@ -78,7 +77,7 @@ namespace osu.Game.Skinning userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files")); - skinModelManager = new SkinModelManager(storage, contextFactory, host, this); + skinModelManager = new SkinModelManager(storage, realm, host, this); var defaultSkins = new[] { @@ -87,12 +86,12 @@ namespace osu.Game.Skinning }; // Ensure the default entries are present. - contextFactory.Write(realm => + realm.Write(r => { foreach (var skin in defaultSkins) { - if (realm.Find(skin.SkinInfo.ID) == null) - realm.Add(skin.SkinInfo.Value); + if (r.Find(skin.SkinInfo.ID) == null) + r.Add(skin.SkinInfo.Value); } }); @@ -110,10 +109,10 @@ namespace osu.Game.Skinning public void SelectRandomSkin() { - contextFactory.Run(realm => + realm.Run(r => { // choose from only user skins, removing the current selection to ensure a new one is chosen. - var randomChoices = realm.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); + var randomChoices = r.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray(); if (randomChoices.Length == 0) { @@ -123,7 +122,7 @@ namespace osu.Game.Skinning var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); - CurrentSkinInfo.Value = chosen.ToLive(contextFactory); + CurrentSkinInfo.Value = chosen.ToLive(realm); }); } @@ -151,7 +150,7 @@ namespace osu.Game.Skinning Name = s.Name + @" (modified)", Creator = s.Creator, InstantiationInfo = s.InstantiationInfo, - }).GetResultSafely(); + }); if (result != null) { @@ -179,7 +178,7 @@ namespace osu.Game.Skinning /// The first result for the provided query, or null if no results were found. public ILive Query(Expression> query) { - return contextFactory.Run(realm => realm.All().FirstOrDefault(query)?.ToLive(contextFactory)); + return realm.Run(r => r.All().FirstOrDefault(query)?.ToLive(realm)); } public event Action SourceChanged; @@ -234,7 +233,7 @@ namespace osu.Game.Skinning AudioManager IStorageResourceProvider.AudioManager => audio; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.Files => userFiles; - RealmContextFactory IStorageResourceProvider.RealmContextFactory => contextFactory; + RealmAccess IStorageResourceProvider.RealmAccess => realm; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); #endregion @@ -278,7 +277,7 @@ namespace osu.Game.Skinning return skinModelManager.Import(archive, lowPriority, cancellationToken); } - public Task> Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public ILive Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return skinModelManager.Import(item, archive, lowPriority, cancellationToken); } @@ -289,10 +288,10 @@ namespace osu.Game.Skinning public void Delete([CanBeNull] Expression> filter = null, bool silent = false) { - contextFactory.Run(realm => + realm.Run(r => { - var items = realm.All() - .Where(s => !s.Protected && !s.DeletePending); + var items = r.All() + .Where(s => !s.Protected && !s.DeletePending); if (filter != null) items = items.Where(filter); diff --git a/osu.Game/Skinning/SkinModelManager.cs b/osu.Game/Skinning/SkinModelManager.cs index a1926913a9..0af31100a9 100644 --- a/osu.Game/Skinning/SkinModelManager.cs +++ b/osu.Game/Skinning/SkinModelManager.cs @@ -27,8 +27,8 @@ namespace osu.Game.Skinning private readonly IStorageResourceProvider skinResources; - public SkinModelManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IStorageResourceProvider skinResources) - : base(storage, contextFactory) + public SkinModelManager(Storage storage, RealmAccess realm, GameHost host, IStorageResourceProvider skinResources) + : base(storage, realm) { this.skinResources = skinResources; @@ -205,7 +205,7 @@ namespace osu.Game.Skinning private void populateMissingHashes() { - ContextFactory.Run(realm => + Realm.Run(realm => { var skinsWithoutHashes = realm.All().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray(); diff --git a/osu.Game/Stores/BeatmapImporter.cs b/osu.Game/Stores/BeatmapImporter.cs index 61178014ef..a6f20c8d4f 100644 --- a/osu.Game/Stores/BeatmapImporter.cs +++ b/osu.Game/Stores/BeatmapImporter.cs @@ -44,8 +44,8 @@ namespace osu.Game.Stores private readonly BeatmapOnlineLookupQueue? onlineLookupQueue; - protected BeatmapImporter(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) - : base(storage, contextFactory) + protected BeatmapImporter(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null) + : base(storage, realm) { this.onlineLookupQueue = onlineLookupQueue; } @@ -165,7 +165,7 @@ namespace osu.Game.Stores public override bool IsAvailableLocally(BeatmapSetInfo model) { - return ContextFactory.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); + return Realm.Run(realm => realm.All().Any(b => b.OnlineID == model.OnlineID)); } public override string HumanisedModelName => "beatmap"; diff --git a/osu.Game/Stores/RealmArchiveModelImporter.cs b/osu.Game/Stores/RealmArchiveModelImporter.cs index 3d8e9f2703..43c1c7c888 100644 --- a/osu.Game/Stores/RealmArchiveModelImporter.cs +++ b/osu.Game/Stores/RealmArchiveModelImporter.cs @@ -59,7 +59,7 @@ namespace osu.Game.Stores protected readonly RealmFileStore Files; - protected readonly RealmContextFactory ContextFactory; + protected readonly RealmAccess Realm; /// /// Fired when the user requests to view the resulting import. @@ -71,11 +71,11 @@ namespace osu.Game.Stores /// public Action? PostNotification { protected get; set; } - protected RealmArchiveModelImporter(Storage storage, RealmContextFactory contextFactory) + protected RealmArchiveModelImporter(Storage storage, RealmAccess realm) { - ContextFactory = contextFactory; + Realm = realm; - Files = new RealmFileStore(contextFactory, storage); + Files = new RealmFileStore(realm, storage); } /// @@ -250,8 +250,10 @@ namespace osu.Game.Stores return null; } - var scheduledImport = Task.Factory.StartNew(async () => await Import(model, archive, lowPriority, cancellationToken).ConfigureAwait(false), - cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap(); + var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, lowPriority, cancellationToken), + cancellationToken, + TaskCreationOptions.HideScheduler, + lowPriority ? import_scheduler_low_priority : import_scheduler); return await scheduledImport.ConfigureAwait(false); } @@ -318,9 +320,9 @@ namespace osu.Game.Stores /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - public virtual Task?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public virtual ILive? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { - return ContextFactory.Run(realm => + return Realm.Run(realm => { cancellationToken.ThrowIfCancellationRequested(); @@ -342,7 +344,8 @@ namespace osu.Game.Stores // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. if (CanSkipImport(existing, item) && - getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f))) + getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)) && + checkAllFilesExist(existing)) { LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); @@ -352,7 +355,7 @@ namespace osu.Game.Stores transaction.Commit(); } - return Task.FromResult((ILive?)existing.ToLive(ContextFactory)); + return existing.ToLive(Realm); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -387,7 +390,7 @@ namespace osu.Game.Stores existing.DeletePending = false; transaction.Commit(); - return Task.FromResult((ILive?)existing.ToLive(ContextFactory)); + return existing.ToLive(Realm); } LogForModel(item, @"Found existing but failed re-use check."); @@ -413,7 +416,7 @@ namespace osu.Game.Stores throw; } - return Task.FromResult((ILive?)item.ToLive(ContextFactory)); + return (ILive?)item.ToLive(Realm); }); } @@ -459,7 +462,6 @@ namespace osu.Game.Stores if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) prefix = string.Empty; - // import files to manager foreach (string file in reader.Filenames) yield return (file, file.Substring(prefix.Length).ToStandardisedPath()); } @@ -519,7 +521,11 @@ namespace osu.Game.Stores // for the best or worst, we copy and import files of a new import before checking whether // it is a duplicate. so to check if anything has changed, we can just compare all File IDs. getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) && - getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)); + getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)) && + checkAllFilesExist(existing); + + private bool checkAllFilesExist(TModel model) => + model.Files.All(f => Files.Storage.Exists(f.File.GetStoragePath())); /// /// Whether this specified path should be removed after successful import. diff --git a/osu.Game/Stores/RealmArchiveModelManager.cs b/osu.Game/Stores/RealmArchiveModelManager.cs index 115fbf721d..57e51b79aa 100644 --- a/osu.Game/Stores/RealmArchiveModelManager.cs +++ b/osu.Game/Stores/RealmArchiveModelManager.cs @@ -24,10 +24,10 @@ namespace osu.Game.Stores { private readonly RealmFileStore realmFileStore; - protected RealmArchiveModelManager(Storage storage, RealmContextFactory contextFactory) - : base(storage, contextFactory) + protected RealmArchiveModelManager(Storage storage, RealmAccess realm) + : base(storage, realm) { - realmFileStore = new RealmFileStore(contextFactory, storage); + realmFileStore = new RealmFileStore(realm, storage); } public void DeleteFile(TModel item, RealmNamedFileUsage file) => @@ -45,7 +45,7 @@ namespace osu.Game.Stores // This method should be removed as soon as all the surrounding pieces support non-detached operations. if (!item.IsManaged) { - var managed = ContextFactory.Context.Find(item.ID); + var managed = Realm.Realm.Find(item.ID); managed.Realm.Write(() => operation(managed)); item.Files.Clear(); @@ -165,7 +165,7 @@ namespace osu.Game.Stores public bool Delete(TModel item) { - return ContextFactory.Run(realm => + return Realm.Run(realm => { if (!item.IsManaged) item = realm.Find(item.ID); @@ -180,7 +180,7 @@ namespace osu.Game.Stores public void Undelete(TModel item) { - ContextFactory.Run(realm => + Realm.Run(realm => { if (!item.IsManaged) item = realm.Find(item.ID); diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index ca371e29be..b5dd3d64e4 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -24,15 +24,15 @@ namespace osu.Game.Stores [ExcludeFromDynamicCompile] public class RealmFileStore { - private readonly RealmContextFactory realmFactory; + private readonly RealmAccess realm; public readonly IResourceStore Store; public readonly Storage Storage; - public RealmFileStore(RealmContextFactory realmFactory, Storage storage) + public RealmFileStore(RealmAccess realm, Storage storage) { - this.realmFactory = realmFactory; + this.realm = realm; Storage = storage.GetStorageForDirectory(@"files"); Store = new StorageBackedResourceStore(Storage); @@ -92,10 +92,10 @@ namespace osu.Game.Stores int removedFiles = 0; // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. - realmFactory.Write(realm => + realm.Write(r => { // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) - var files = realm.All().ToList(); + var files = r.All().ToList(); foreach (var file in files) { @@ -108,7 +108,7 @@ namespace osu.Game.Stores { removedFiles++; Storage.Delete(file.GetStoragePath()); - realm.Remove(file); + r.Remove(file); } catch (Exception e) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 3d6240bc98..e6528a83bd 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -77,12 +77,12 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmContextFactory realmContextFactory) + private void load(GameplayClock clock, CancellationToken? cancellationToken, GameHost host, RealmAccess realm) { if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(new RealmFileStore(realmContextFactory, host.Storage).Store), false, scaleAdjust: 1)); + dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(new RealmFileStore(realm, host.Storage).Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 10cb210f4d..f7e154b5e7 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Beatmaps public IResourceStore Files => userSkinResourceStore; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; - RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; + RealmAccess IStorageResourceProvider.RealmAccess => null; #endregion diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index 754c9044e8..bdb171c528 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using osu.Framework; using osu.Framework.Testing; namespace osu.Game.Tests @@ -20,7 +21,10 @@ namespace osu.Game.Tests /// Whether to bypass directory cleanup on host disposal. Should be used only if a subsequent test relies on the files still existing. /// The name of the calling method, used for test file isolation and clean-up. public CleanRunHeadlessGameHost(bool bindIPC = false, bool realtime = true, bool bypassCleanup = false, [CallerMemberName] string callingMethodName = @"") - : base($"{callingMethodName}-{Guid.NewGuid()}", bindIPC, realtime, bypassCleanup: bypassCleanup) + : base($"{callingMethodName}-{Guid.NewGuid()}", new HostOptions + { + BindIPC = bindIPC, + }, bypassCleanup: bypassCleanup, realtime: realtime) { } diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 6cc009514d..f7d62a8694 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -43,26 +43,14 @@ namespace osu.Game.Tests.Visual }; private TestBeatmapManager testBeatmapManager; - private WorkingBeatmap working; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio, RulesetStore rulesets) { Add(logo); - working = CreateWorkingBeatmap(Ruleset.Value); - if (IsolateSavingFromDatabase) - Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Beatmap.Value = working; - if (testBeatmapManager != null) - testBeatmapManager.TestBeatmap = working; + Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); } protected virtual bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true @@ -78,6 +66,11 @@ namespace osu.Game.Tests.Visual protected virtual void LoadEditor() { + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + if (testBeatmapManager != null) + testBeatmapManager.TestBeatmap = Beatmap.Value; + LoadScreen(editorLoader = new TestEditorLoader()); } @@ -126,14 +119,14 @@ namespace osu.Game.Tests.Visual { public WorkingBeatmap TestBeatmap; - public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host, WorkingBeatmap defaultBeatmap) - : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) + public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host, WorkingBeatmap defaultBeatmap) + : base(storage, realm, rulesets, api, audioManager, resources, host, defaultBeatmap) { } - protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapOnlineLookupQueue onlineLookupQueue) { - return new TestBeatmapModelManager(storage, contextFactory, rulesets, onlineLookupQueue); + return new TestBeatmapModelManager(storage, realm, rulesets, onlineLookupQueue); } protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) @@ -157,8 +150,8 @@ namespace osu.Game.Tests.Visual internal class TestBeatmapModelManager : BeatmapModelManager { - public TestBeatmapModelManager(Storage storage, RealmContextFactory databaseContextFactory, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) - : base(databaseContextFactory, storage, beatmapOnlineLookupQueue) + public TestBeatmapModelManager(Storage storage, RealmAccess databaseAccess, RulesetStore rulesetStore, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue) + : base(databaseAccess, storage, beatmapOnlineLookupQueue) { } diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 6a11bd3fea..ebbd9bb5dd 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -72,8 +73,11 @@ namespace osu.Game.Tests.Visual [TearDownSteps] public void TearDownSteps() { - AddStep("exit game", () => Game.Exit()); - AddUntilStep("wait for game exit", () => Game.Parent == null); + if (DebugUtils.IsNUnitRunning) + { + AddStep("exit game", () => Game.Exit()); + AddUntilStep("wait for game exit", () => Game.Parent == null); + } } protected void CreateGame() diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index da8af49158..42e96f80ca 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -75,9 +75,9 @@ namespace osu.Game.Tests.Visual /// /// In interactive runs (ie. VisualTests) this will use the user's database if is not set to true. /// - protected RealmContextFactory ContextFactory => contextFactory.Value; + protected RealmAccess Realm => realm.Value; - private Lazy contextFactory; + private Lazy realm; /// /// Whether a fresh storage should be initialised per test (method) run. @@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual Resources = parent.Get().Resources; - contextFactory = new Lazy(() => new RealmContextFactory(LocalStorage, "client")); + realm = new Lazy(() => new RealmAccess(LocalStorage, "client")); RecycleLocalStorage(false); diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index c44a848275..b6f6ca6daa 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -48,7 +49,11 @@ namespace osu.Game.Tests.Visual public virtual void SetUpSteps() => addExitAllScreensStep(); [TearDownSteps] - public virtual void TearDownSteps() => addExitAllScreensStep(); + public virtual void TearDownSteps() + { + if (DebugUtils.IsNUnitRunning) + addExitAllScreensStep(); + } private void addExitAllScreensStep() { diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index a080f47d66..cd675e467b 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); - RealmContextFactory IStorageResourceProvider.RealmContextFactory => null; + RealmAccess IStorageResourceProvider.RealmAccess => null; #endregion diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index d63b3d48b2..6aa75ec147 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true, })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index 7db834bf83..e5debc0683 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.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.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -23,6 +24,12 @@ namespace osu.Game.Users.Drawables /// public bool ShowPlaceholderOnNull = true; + /// + /// Perform an action in addition to showing the country ranking. + /// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX). + /// + public Action Action; + public UpdateableFlag(Country country = null) { Country = country; @@ -52,6 +59,7 @@ namespace osu.Game.Users.Drawables protected override bool OnClick(ClickEvent e) { + Action?.Invoke(); rankingsOverlay?.ShowCountry(Country); return true; } diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index fc5e1eca5f..d0f693c37c 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -53,7 +53,8 @@ namespace osu.Game.Users protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) { - Size = new Vector2(39, 26) + Size = new Vector2(39, 26), + Action = Action, }; protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 758575e74a..af5d8a5920 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 5925581e28..2bcdea61b3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -60,7 +60,7 @@ - + @@ -83,7 +83,7 @@ - +