1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 04:07:25 +08:00

Merge branch 'ppy:master' into Wiggle-strength-duration

This commit is contained in:
MK56 2022-01-25 19:57:14 +01:00 committed by GitHub
commit c17ff49427
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 1963 additions and 1063 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.118.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.125.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -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;

View File

@ -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<BeatmapSetInfo>().First();
var beatmapSet = r.All<BeatmapSetInfo>().First();
for (int i = 0; i < ReadsPerFetch; i++)
{
@ -61,7 +61,7 @@ namespace osu.Game.Benchmarks
{
try
{
var beatmapSet = realmFactory.Context.All<BeatmapSetInfo>().First();
var beatmapSet = realm.Realm.All<BeatmapSetInfo>().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<BeatmapSetInfo>().First().ToLive(realmFactory);
var beatmapSet = r.All<BeatmapSetInfo>().First().ToLive(realm);
for (int i = 0; i < ReadsPerFetch; i++)
{
@ -100,7 +100,7 @@ namespace osu.Game.Benchmarks
{
try
{
var beatmapSet = realmFactory.Context.All<BeatmapSetInfo>().First().ToLive(realmFactory);
var beatmapSet = realm.Realm.All<BeatmapSetInfo>().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<BeatmapSetInfo>().First().Detach();
var beatmapSet = r.All<BeatmapSetInfo>().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();
}

View File

@ -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<float> SizeMultiplier { get; } = new BindableNumber<float>
{
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<int> 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";

View File

@ -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<float> SizeMultiplier { get; } = new BindableNumber<float>
{
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<int> e)
{
this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "RectangularFlashlight";

View File

@ -0,0 +1,98 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<EditorBeatmap>().FirstOrDefault();
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().FirstOrDefault();
private Slider slider => editorBeatmap.HitObjects.OfType<Slider>().FirstOrDefault();
private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType<TimelineHitObjectBlueprint>().FirstOrDefault();
private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType<DifficultyPointPiece>().First();
private IndeterminateSliderWithTextBoxInput<double> velocityTextBox => Game.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().First().ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().First();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private bool editorComponentsReady => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true
&& editor?.ChildrenOfType<Playfield>().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<Playfield>().First().ScreenSpaceDrawQuad.Centre));
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().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);
}
}
}

View File

@ -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<OsuHitObject> drawableRuleset)
{
base.ApplyToDrawableRuleset(drawableRuleset);
flashlight.FollowDelay = FollowDelay.Value;
}
[SettingSource("Follow delay", "Milliseconds until the flashlight reaches the cursor")]
public BindableNumber<double> 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<float> SizeMultiplier { get; } = new BindableNumber<float>
{
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<bool> 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<int> 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";

View File

@ -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<float> SizeMultiplier { get; } = new BindableNumber<float>
{
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<int> e)

View File

@ -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;

View File

@ -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);

View File

@ -53,9 +53,9 @@ namespace osu.Game.Tests.Beatmaps.IO
private static void ensureLoaded(OsuGameBase osu, int timeout = 60000)
{
var realmContextFactory = osu.Dependencies.Get<RealmContextFactory>();
var realm = osu.Dependencies.Get<RealmAccess>();
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();

View File

@ -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
{

View File

@ -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<BeatmapSetInfo>? 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<BeatmapSetInfo>? 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<BeatmapSetInfo>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
imported = await importer.Import(reader);
Assert.AreEqual(1, realmFactory.Context.All<BeatmapSetInfo>().Count());
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
Assert.NotNull(imported);
Debug.Assert(imported != null);
imported.PerformWrite(s => s.DeletePending = true);
Assert.AreEqual(1, realmFactory.Context.All<BeatmapSetInfo>().Count(s => s.DeletePending));
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count(s => s.DeletePending));
}
});
Logger.Log("Running with no work to purge pending deletions");
RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All<BeatmapSetInfo>().Count()); });
RunTestWithRealm((realm, _) => { Assert.AreEqual(0, realm.Realm.All<BeatmapSetInfo>().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<BeatmapSetInfo>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
var imported = realm.Realm.All<BeatmapSetInfo>().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<RulesetInfo>().First();
var ruleset = realm.Realm.All<RulesetInfo>().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<BeatmapSetInfo>().First();
BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First();
var beatmapToUpdate = setToUpdate.Beatmaps.First();
realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated");
realm.Realm.Write(() => beatmapToUpdate.DifficultyName = "updated");
BeatmapInfo updatedInfo = realmFactory.Context.All<BeatmapInfo>().First(b => b.ID == beatmapToUpdate.ID);
BeatmapInfo updatedInfo = realm.Realm.All<BeatmapInfo>().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)
{
}

View File

@ -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));

View File

@ -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<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
var subscription = r.All<BeatmapInfo>().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<TimeoutException>(() =>
{
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);
}
}
}

View File

@ -21,11 +21,11 @@ namespace osu.Game.Tests.Database
[Test]
public void TestLiveEquality()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
ILive<BeatmapInfo> beatmap = realmFactory.Run(realm => realm.Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory));
ILive<BeatmapInfo> beatmap = realm.Run(r => r.Write(_ => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realm));
ILive<BeatmapInfo> beatmap2 = realmFactory.Run(realm => realm.All<BeatmapInfo>().First().ToLive(realmFactory));
ILive<BeatmapInfo> beatmap2 = realm.Run(r => r.All<BeatmapInfo>().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<BeatmapInfo>? 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<BeatmapInfo>? 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<BeatmapInfo>? 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<BeatmapInfo>? 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<InvalidOperationException>(() =>
{
@ -207,16 +207,16 @@ namespace osu.Game.Tests.Database
[Test]
public void TestValueAccessWithoutOpenContextFails()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
ILive<BeatmapInfo>? 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<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
ILive<BeatmapInfo>? 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<BeatmapInfo> sender, ChangeSet changes, Exception error)

View File

@ -0,0 +1,138 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<BeatmapSetInfo>? resolvedItems = null;
ChangeSet? lastChanges = null;
RunTestWithRealm((realm, _) =>
{
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), 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<BeatmapSetInfo>();
r.RemoveAll<RulesetInfo>();
});
realm.Run(r => r.Refresh());
if (shouldArrive)
Assert.That(lastChanges?.DeletedIndices, Has.One.Items);
else
Assert.That(lastChanges, Is.Null);
}
});
void onChanged(IRealmCollection<BeatmapSetInfo> 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<BeatmapSetInfo>().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);
});
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Database
storage.DeleteDirectory(string.Empty);
}
protected void RunTestWithRealm(Action<RealmContextFactory, OsuStorage> testAction, [CallerMemberName] string caller = "")
protected void RunTestWithRealm(Action<RealmAccess, OsuStorage> 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<RealmContextFactory, Storage, Task> testAction, [CallerMemberName] string caller = "")
protected void RunTestWithRealmAsync(Func<RealmAccess, Storage, Task> 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

View File

@ -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<RulesetInfo>().Count());
Assert.AreEqual(4, realm.Realm.All<RulesetInfo>().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<RulesetInfo>().Count());
Assert.AreEqual(4, realm.Realm.All<RulesetInfo>().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);

View File

@ -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<RealmKeyBinding>();
var results = r.All<RealmKeyBinding>();
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<RulesetInfo>());
realmContextFactory.Run(outerRealm =>
realm.Run(outerRealm =>
{
var backBinding = outerRealm.All<RealmKeyBinding>().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);
}

View File

@ -261,7 +261,7 @@ namespace osu.Game.Tests.Gameplay
public AudioManager AudioManager => Audio;
public IResourceStore<byte[]> Files => null;
public new IResourceStore<byte[]> Resources => base.Resources;
public RealmContextFactory RealmContextFactory => null;
public RealmAccess RealmAccess => null;
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null;
#endregion

View File

@ -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);
}
}

View File

@ -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}"));

View File

@ -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<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(rulesets = new RulesetStore(Realm));
Dependencies.CacheAs<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs<BeatmapModelDownloader>(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<BeatmapSetInfo>());
ContextFactory.Write(r => r.RemoveAll<BeatmapInfo>());
Realm.Write(r => r.RemoveAll<BeatmapSetInfo>());
Realm.Write(r => r.RemoveAll<BeatmapInfo>());
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<bool> AllowImport = new TaskCompletionSource<bool>();
public Task<ILive<BeatmapSetInfo>> CurrentImportTask { get; private set; }
public ILive<BeatmapSetInfo> CurrentImport { get; private set; }
public TestBeatmapManager(Storage storage, RealmContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> 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<byte[]> 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<ILive<BeatmapSetInfo>> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
public override ILive<BeatmapSetInfo> 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));
}
}
}

View File

@ -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);

View File

@ -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<ScoreInfo> 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<ScoreManager>();
await scoreManager.Import(score, archive);
scoreManager.Import(score, archive);
return scoreManager.Query(_ => true);
}

View File

@ -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();

View File

@ -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();

View File

@ -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()

View File

@ -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()

View File

@ -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,12 +41,14 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true);
AddStep("Set beat divisor", () => editor.Dependencies.Get<BindableBeatDivisor>().Value = 16);
AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7);
AddStep("Set artist and title", () =>
{
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
});
AddStep("Set author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username = "author");
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
@ -64,6 +67,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
checkMutations();
AddAssert("Beatmap has correct .osu file path", () => editorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu");
AddStep("Exit", () => InputManager.Key(Key.Escape));
@ -86,8 +90,10 @@ 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");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
}
}

View File

@ -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();
}

View File

@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<OsuTextBox>().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");
}
}
}

View File

@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay
Add(new ModNightcore<HitObject>.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));
}
}
}

View File

@ -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);

View File

@ -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<OsuPasswordTextBox>().First().Text = "password");
AddStep("submit", () => loginPanel.ChildrenOfType<OsuButton>().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<OsuPasswordTextBox>().First().Text = "password");
AddStep("submit", () => loginPanel.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
AddStep("click on flag", () =>
{
InputManager.MoveMouseTo(loginPanel.ChildrenOfType<UpdateableFlag>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("hide requested", () => hideCount == 1);
}
}
}

View File

@ -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", () =>
{

View File

@ -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()

View File

@ -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()));

View File

@ -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()

View File

@ -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<BeatmapInfo>();
@ -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()

View File

@ -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();

View File

@ -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]

View File

@ -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()

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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()

View File

@ -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()

View File

@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Navigation
.ChildrenOfType<KeyBindingPanel>().SingleOrDefault();
private RealmKeyBinding firstOsuRulesetKeyBindings => Game.Dependencies
.Get<RealmContextFactory>().Context
.Get<RealmAccess>().Realm
.All<RealmKeyBinding>()
.AsEnumerable()
.First(k => k.RulesetName == "osu" && k.ActionInt == 0);

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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<BeatmapInfo>()
.Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0)
.FirstOrDefault();
var beatmapInfo = r.All<BeatmapInfo>()
.Filter($"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0", 0)
.FirstOrDefault();
if (beatmapInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);

View File

@ -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<AudioManager>(), Resources, dependencies.Get<GameHost>(), 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<AudioManager>(), Resources, dependencies.Get<GameHost>(), 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);
});
}

View File

@ -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<BeatmapSetInfo> beatmapSets) => beatmapSets.All(set => set != null);

View File

@ -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();

View File

@ -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));
});
}

View File

@ -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();
}

View File

@ -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<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<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<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => 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<ScoreInfo>().Where(s => s.DeletePending).ToList());
scoreManager.Undelete(r.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
});
leaderboard.Scores = null;

View File

@ -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");

View File

@ -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));

View File

@ -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;

View File

@ -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<byte[]> gameResources, GameHost? host = null, WorkingBeatmap? defaultBeatmap = null, bool performOnlineLookups = false)
public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> 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);
/// <summary>
/// Create a new <see cref="WorkingBeatmap"/>.
@ -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
/// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
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>(beatmapInfo.ID);
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
beatmapInfo.Hidden = true;
transaction.Commit();
@ -138,12 +137,12 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
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>(beatmapInfo.ID);
beatmapInfo = r.Find<BeatmapInfo>(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<BeatmapInfo>().Where(b => b.Hidden))
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Hidden))
beatmap.Hidden = false;
transaction.Commit();
@ -171,10 +170,10 @@ namespace osu.Game.Beatmaps
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
{
return contextFactory.Run(realm =>
return realm.Run(r =>
{
realm.Refresh();
return realm.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
r.Refresh();
return r.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
});
}
@ -185,7 +184,7 @@ namespace osu.Game.Beatmaps
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public ILive<BeatmapSetInfo>? QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query)
{
return contextFactory.Run(realm => realm.All<BeatmapSetInfo>().FirstOrDefault(query)?.ToLive(contextFactory));
return realm.Run(r => r.All<BeatmapSetInfo>().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<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
{
contextFactory.Run(realm =>
realm.Run(r =>
{
var items = realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
var items = r.All<BeatmapSetInfo>().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<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
realm.Run(r => beatmapModelManager.Undelete(r.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
}
public void Undelete(List<BeatmapSetInfo> items, bool silent = false)
@ -295,7 +294,7 @@ namespace osu.Game.Beatmaps
return beatmapModelManager.Import(archive, lowPriority, cancellationToken);
}
public Task<ILive<BeatmapSetInfo>?> Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
public ILive<BeatmapSetInfo>? 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<BeatmapInfo>(importedBeatmap.ID)?.Detach();
var refetch = r.Find<BeatmapInfo>(importedBeatmap.ID)?.Detach();
if (refetch != null)
importedBeatmap = refetch;

View File

@ -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)
{
}
@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps
private static string getFilename(BeatmapInfo beatmapInfo)
{
var metadata = beatmapInfo.Metadata;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
}
/// <summary>
@ -98,12 +98,12 @@ namespace osu.Game.Beatmaps
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query)
{
return ContextFactory.Run(realm => realm.All<BeatmapInfo>().FirstOrDefault(query)?.Detach());
return Realm.Run(realm => realm.All<BeatmapInfo>().FirstOrDefault(query)?.Detach());
}
public void Update(BeatmapSetInfo item)
{
ContextFactory.Write(realm =>
Realm.Write(realm =>
{
var existing = realm.Find<BeatmapSetInfo>(item.ID);
item.CopyChangesToRealm(existing);

View File

@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// The time signature at this control point.
/// </summary>
public readonly Bindable<TimeSignatures> TimeSignatureBindable = new Bindable<TimeSignatures>(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
public readonly Bindable<TimeSignature> TimeSignatureBindable = new Bindable<TimeSignature>(TimeSignature.SimpleQuadruple);
/// <summary>
/// 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
/// <summary>
/// The time signature at this control point.
/// </summary>
public TimeSignatures TimeSignature
public TimeSignature TimeSignature
{
get => TimeSignatureBindable.Value;
set => TimeSignatureBindable.Value = value;

View File

@ -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)

View File

@ -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<HitObject> hitObjects)

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Game.Beatmaps.Timing
{
/// <summary>
/// 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.
/// </summary>
public class TimeSignature : IEquatable<TimeSignature>
{
/// <summary>
/// The numerator of a signature.
/// </summary>
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;
}
}

View File

@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,

View File

@ -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<byte[]> IStorageResourceProvider.Files => files;
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);

View File

@ -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;
}
}
}

View File

@ -27,13 +27,15 @@ namespace osu.Game.Database
{
internal class EFToRealmMigrator : CompositeDrawable
{
public bool FinishedMigrating { get; private set; }
public Task<bool> MigrationCompleted => migrationCompleted.Task;
private readonly TaskCompletionSource<bool> migrationCompleted = new TaskCompletionSource<bool>();
[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<BeatmapSetInfo>();
r.RemoveAll<BeatmapInfo>();
r.RemoveAll<BeatmapMetadata>();
r.RemoveAll<ScoreInfo>();
});
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<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
if (realm.All<BeatmapSetInfo>().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<RulesetInfo>(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<RulesetInfo>(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<ScoreInfo>().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<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash);
var ruleset = realm.Find<RulesetInfo>(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<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash);
var ruleset = r.Find<RulesetInfo>(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<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
if (!realm.All<SkinInfo>().Any(s => !s.Protected))
// note that this cannot be written as: `r.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
if (!r.All<SkinInfo>().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<RealmRulesetSetting>().Any())
if (!r.All<RealmRulesetSetting>().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,

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<T> : IRealmCollection<T>
{
private IList<T> emptySet => Array.Empty<T>();
public IEnumerator<T> 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<T> Freeze() => throw new NotImplementedException();
public IDisposable SubscribeForNotifications(NotificationCallbackDelegate<T> 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();
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Database
/// <param name="archive">An optional archive to use for model population.</param>
/// <param name="lowPriority">Whether this is a low priority import.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default);
ILive<TModel>? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default);
/// <summary>
/// A user displayable name for the model type associated with this manager.

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
/// </summary>
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1);
private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1);
private readonly ThreadLocal<bool> currentThreadCanCreateContexts = new ThreadLocal<bool>();
private readonly ThreadLocal<bool> currentThreadCanCreateRealmInstances = new ThreadLocal<bool>();
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>(@"Realm", @"Contexts (Created)");
/// <summary>
/// Holds a map of functions registered via <see cref="RegisterCustomSubscription"/> and <see cref="RegisterForNotifications{T}"/> 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 <see cref="IDisposable"/> 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 <see cref="IDisposable"/>).
/// </summary>
private readonly Dictionary<Func<Realm, IDisposable?>, IDisposable?> customSubscriptionsResetMap = new Dictionary<Func<Realm, IDisposable?>, IDisposable?>();
private readonly object contextLock = new object();
/// <summary>
/// Holds a map of functions registered via <see cref="RegisterForNotifications{T}"/> 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.
/// </summary>
private readonly Dictionary<Func<Realm, IDisposable?>, Action> notificationsResetMap = new Dictionary<Func<Realm, IDisposable?>, Action>();
private Realm? context;
private static readonly GlobalStatistic<int> realm_instances_created = GlobalStatistics.Get<int>(@"Realm", @"Instances (Created)");
public Realm Context
private static readonly GlobalStatistic<int> total_subscriptions = GlobalStatistics.Get<int>(@"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<bool> current_thread_subscriptions_allowed = new ThreadLocal<bool>();
/// <summary>
/// Construct a new instance of a realm context factory.
/// Construct a new instance.
/// </summary>
/// <param name="storage">The game storage which will be used to create the realm backing file.</param>
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
/// <param name="efContextFactory">An EF factory used only for migration purposes.</param>
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<ScoreInfo>().Where(s => s.DeletePending);
@ -172,34 +203,28 @@ namespace osu.Game.Database
/// <summary>
/// Run work on realm with a return value.
/// </summary>
/// <remarks>
/// Handles correct context management automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
/// <typeparam name="T">The return type.</typeparam>
public T Run<T>(Func<Realm, T> action)
{
if (ThreadSafety.IsUpdateThread)
return action(Context);
return action(Realm);
using (var realm = createContext())
using (var realm = getRealmInstance())
return action(realm);
}
/// <summary>
/// Run work on realm.
/// </summary>
/// <remarks>
/// Handles correct context management automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
public void Run(Action<Realm> 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
/// <summary>
/// Write changes to realm.
/// </summary>
/// <remarks>
/// Handles correct context management and transaction committing automatically.
/// </remarks>
/// <param name="action">The work to run.</param>
public void Write(Action<Realm> 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()
/// <summary>
/// Subscribe to a realm collection and begin watching for asynchronous changes.
/// </summary>
/// <remarks>
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
///
/// 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 <c>null</c> <see cref="ChangeSet"/> will be invoked.
/// - On recycle end, a standard initial realm callback will arrive, with <c>null</c> <see cref="ChangeSet"/> and an up-to-date collection.
/// </remarks>
/// <param name="query">The <see cref="IQueryable{T}"/> to observe for changes.</param>
/// <typeparam name="T">Type of the elements in the list.</typeparam>
/// <param name="callback">The callback to be invoked with the updated <see cref="IRealmCollection{T}"/>.</param>
/// <returns>
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
/// </returns>
/// <seealso cref="IRealmCollection{T}.SubscribeForNotifications"/>
public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase
{
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
lock (realmLock)
{
Func<Realm, IDisposable?> 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<T>(), null, null));
return RegisterCustomSubscription(action);
}
}
/// <summary>
/// Run work on realm that will be run every time the update thread realm instance gets recycled.
/// </summary>
/// <param name="action">The work to run. Return value should be an <see cref="IDisposable"/> from QueryAsyncWithNotifications, or an <see cref="InvokeOnDisposal"/> to clean up any bindings.</param>
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> 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<Realm, IDisposable?> 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;
}
}
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>
/// Flush any active contexts and block any further writes.
/// Flush any active realm instances and block any further writes.
/// </summary>
/// <remarks>
/// 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<RealmContextFactory>(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;
}

View File

@ -24,17 +24,17 @@ namespace osu.Game.Database
/// </summary>
private readonly T data;
private readonly RealmContextFactory realmFactory;
private readonly RealmAccess realm;
/// <summary>
/// Construct a new instance of live realm data.
/// </summary>
/// <param name="data">The realm data.</param>
/// <param name="realmFactory">The realm factory the data was sourced from. May be null for an unmanaged object.</param>
public RealmLive(T data, RealmContextFactory realmFactory)
/// <param name="realm">The realm factory the data was sourced from. May be null for an unmanaged object.</param>
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)));
}
/// <summary>
@ -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<T>(ID);
return realm.Realm.Find<T>(ID);
}
}

View File

@ -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<T>(realmObject);
}
public static List<ILive<T>> ToLive<T>(this IEnumerable<T> realmList, RealmContextFactory realmContextFactory)
public static List<ILive<T>> ToLive<T>(this IEnumerable<T> realmList, RealmAccess realm)
where T : RealmObject, IHasGuidPrimaryKey
{
return realmList.Select(l => new RealmLive<T>(l, realmContextFactory)).Cast<ILive<T>>().ToList();
return realmList.Select(l => new RealmLive<T>(l, realm)).Cast<ILive<T>>().ToList();
}
public static ILive<T> ToLive<T>(this T realmObject, RealmContextFactory realmContextFactory)
public static ILive<T> ToLive<T>(this T realmObject, RealmAccess realm)
where T : RealmObject, IHasGuidPrimaryKey
{
return new RealmLive<T>(realmObject, realmContextFactory);
return new RealmLive<T>(realmObject, realm);
}
/// <summary>
@ -272,9 +271,8 @@ namespace osu.Game.Database
public static IDisposable? QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> 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);
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.IO
/// <summary>
/// Access realm.
/// </summary>
RealmContextFactory RealmContextFactory { get; }
RealmAccess RealmAccess { get; }
/// <summary>
/// Create a texture loader store based on an underlying data store.

View File

@ -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<RealmKeyBinding> realmKeyBindings;
[Resolved]
private RealmContextFactory realmFactory { get; set; }
private RealmAccess realm { get; set; }
public override IEnumerable<IKeyBinding> 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<RealmKeyBinding>()
.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<RealmKeyBinding> queryRealmKeyBindings(Realm realm)
{
string rulesetName = ruleset?.ShortName;
return realm.All<RealmKeyBinding>()
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
}
protected override void ReloadMappings()
private void reloadMappings(IQueryable<RealmKeyBinding> 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();
}
}
}

View File

@ -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<string> combinations = new List<string>();
realmFactory.Run(context =>
realm.Run(context =>
{
foreach (var action in context.All<RealmKeyBinding>().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction))
{
@ -56,21 +56,21 @@ namespace osu.Game.Input
/// <param name="rulesets">The rulesets to populate defaults from.</param>
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> 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<RealmKeyBinding>().ToList();
var existingBindings = r.All<RealmKeyBinding>().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();

View File

@ -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<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) =>
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) =>
{
if (items.Any())
Schedule(() => UpdateState(DownloadState.LocallyAvailable));

View File

@ -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!;
/// <summary>
/// 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<BeatmapInfo>()
.Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum);
return realm.Realm
.All<BeatmapInfo>()
.Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", onlineId, checksum);
}
protected override void Dispose(bool isDisposing)

View File

@ -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<ScoreInfo>().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) =>
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) =>
{
if (items.Any())
Schedule(() => UpdateState(DownloadState.LocallyAvailable));

View File

@ -149,7 +149,7 @@ namespace osu.Game
private MultiplayerClient multiplayerClient;
private RealmContextFactory realmFactory;
private RealmAccess realm;
protected override Container<Drawable> 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<IRulesetStore>(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<ISkinSource>(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<IRulesetConfigCache>(rulesetConfigCache = new RulesetConfigCache(realmFactory, RulesetStore));
dependencies.CacheAs<IRulesetConfigCache>(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();
}
}
}

View File

@ -30,16 +30,7 @@ namespace osu.Game.Overlays
[Resolved]
private BeatmapManager beatmaps { get; set; }
public IBindableList<BeatmapSetInfo> 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<BeatmapSetInfo> BeatmapSets => beatmapSets;
/// <summary>
/// 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<BeatmapSetInfo> queryRealmBeatmapSets() =>
realm.Realm
.All<BeatmapSetInfo>()
.Where(s => !s.DeletePending);
protected override void LoadComplete()
{
base.LoadComplete();
var availableBeatmaps = realmFactory.Context
.All<BeatmapSetInfo>()
.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<BeatmapSetInfo> 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());

View File

@ -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)

View File

@ -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
/// </summary>
public event Action Closed;
public abstract LocalisableString Text { get; set; }
/// <summary>
/// Whether this notification should forcefully display itself.
/// </summary>

View File

@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Notifications
private LocalisableString text;
public LocalisableString Text
public override LocalisableString Text
{
get => text;
set

View File

@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Notifications
{
private LocalisableString text;
public LocalisableString Text
public override LocalisableString Text
{
get => text;
set

View File

@ -1,9 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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");
}
};
}
}

View File

@ -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<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString);
var binding = r.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
r.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString);
});
}

View File

@ -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<RealmKeyBinding>()
.Where(b => b.RulesetName == rulesetName && b.Variant == variant)
.Detach());
var bindings = realm.Run(r => r.All<RealmKeyBinding>()
.Where(b => b.RulesetName == rulesetName && b.Variant == variant)
.Detach());
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
{

View File

@ -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<SkinInfo> realmSkins;
private IQueryable<SkinInfo> queryRealmSkins() =>
realm.Realm.All<SkinInfo>()
.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<SkinInfo>()
.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);

View File

@ -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<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value);
var realmKeyBinding = realm.Realm.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value);
if (realmKeyBinding != null)
{

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Configuration
public abstract class RulesetConfigManager<TLookup> : ConfigManager<TLookup>, 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<RealmRulesetSetting>().Where(b => b.RulesetName == rulesetName && b.Variant == variant).ToList();
databasedSettings = realm.Realm.All<RealmRulesetSetting>().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<RealmRulesetSetting>().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString());
var setting = r.All<RealmRulesetSetting>().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);
}

View File

@ -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)

Some files were not shown because too many files have changed in this diff Show More