1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 10:33:07 +08:00

Merge branch 'master' into expandable-controls

This commit is contained in:
Salman Ahmed 2022-01-26 10:36:22 +03:00
commit a34b8fc4d0
157 changed files with 2863 additions and 1567 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

@ -0,0 +1,141 @@
// 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 System.Threading;
using BenchmarkDotNet.Attributes;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources;
namespace osu.Game.Benchmarks
{
public class BenchmarkRealmReads : BenchmarkTest
{
private TemporaryNativeStorage storage;
private RealmAccess realm;
private UpdateThread updateThread;
[Params(1, 100, 1000)]
public int ReadsPerFetch { get; set; }
public override void SetUp()
{
storage = new TemporaryNativeStorage("realm-benchmark");
storage.DeleteDirectory(string.Empty);
realm = new RealmAccess(storage, "client");
realm.Run(r =>
{
realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo })));
});
updateThread = new UpdateThread(() => { }, null);
updateThread.Start();
}
[Benchmark]
public void BenchmarkDirectPropertyRead()
{
realm.Run(r =>
{
var beatmapSet = r.All<BeatmapSetInfo>().First();
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.Beatmaps.First().Hash;
}
});
}
[Benchmark]
public void BenchmarkDirectPropertyReadUpdateThread()
{
var done = new ManualResetEventSlim();
updateThread.Scheduler.Add(() =>
{
try
{
var beatmapSet = realm.Realm.All<BeatmapSetInfo>().First();
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.Beatmaps.First().Hash;
}
}
finally
{
done.Set();
}
});
done.Wait();
}
[Benchmark]
public void BenchmarkRealmLivePropertyRead()
{
realm.Run(r =>
{
var beatmapSet = r.All<BeatmapSetInfo>().First().ToLive(realm);
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.PerformRead(b => b.Beatmaps.First().Hash);
}
});
}
[Benchmark]
public void BenchmarkRealmLivePropertyReadUpdateThread()
{
var done = new ManualResetEventSlim();
updateThread.Scheduler.Add(() =>
{
try
{
var beatmapSet = realm.Realm.All<BeatmapSetInfo>().First().ToLive(realm);
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.PerformRead(b => b.Beatmaps.First().Hash);
}
}
finally
{
done.Set();
}
});
done.Wait();
}
[Benchmark]
public void BenchmarkDetachedPropertyRead()
{
realm.Run(r =>
{
var beatmapSet = r.All<BeatmapSetInfo>().First().Detach();
for (int i = 0; i < ReadsPerFetch; i++)
{
string _ = beatmapSet.Beatmaps.First().Hash;
}
});
}
[GlobalCleanup]
public void Cleanup()
{
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

@ -1,91 +0,0 @@
// 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.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public class TestSceneEditorSaving : OsuGameTestScene
{
private Screens.Edit.Editor editor => Game.ChildrenOfType<Screens.Edit.Editor>().FirstOrDefault();
private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap));
/// <summary>
/// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select.
/// Emphasis is placed on <see cref="BeatmapDifficulty.SliderMultiplier"/>, since taiko has special handling for it to keep compatibility with stable.
/// </summary>
[Test]
public void TestNewBeatmapSaveThenLoad()
{
AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType<MetadataSection>().FirstOrDefault()?.IsLoaded == true);
// We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true);
AddStep("Set slider multiplier", () => editorBeatmap.Difficulty.SliderMultiplier = 2);
AddStep("Set artist and title", () =>
{
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
});
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
checkMutations();
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
checkMutations();
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
PushAndConfirm(() => new PlaySongSelect());
AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
checkMutations();
}
private void checkMutations()
{
AddAssert("Beatmap has correct slider multiplier", () =>
{
// we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use.
// therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct.
var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty();
taikoDifficulty.CopyFrom(editorBeatmap.Difficulty);
return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2);
});
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
}
}
}

View File

@ -0,0 +1,38 @@
// 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 NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public class TestSceneTaikoEditorSaving : EditorSavingTestScene
{
protected override Ruleset CreateRuleset() => new TaikoRuleset();
[Test]
public void TestTaikoSliderMultiplier()
{
AddStep("Set slider multiplier", () => EditorBeatmap.Difficulty.SliderMultiplier = 2);
SaveEditor();
AddAssert("Beatmap has correct slider multiplier", assertTaikoSliderMulitplier);
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct slider multiplier", assertTaikoSliderMulitplier);
bool assertTaikoSliderMulitplier()
{
// we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use.
// therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct.
var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty();
taikoDifficulty.CopyFrom(EditorBeatmap.Difficulty);
return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2);
}
}
}
}

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,10 +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>();
using (var realm = realmContextFactory.CreateContext())
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,12 +38,12 @@ 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;
Live<BeatmapSetInfo>? beatmapSet;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
beatmapSet = await importer.Import(reader);
@ -82,12 +82,12 @@ 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;
Live<BeatmapSetInfo>? beatmapSet;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
beatmapSet = await importer.Import(reader);
@ -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;
Live<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,33 +198,33 @@ 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();
ILive<BeatmapSetInfo>? importedSet;
Live<BeatmapSetInfo>? importedSet;
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.CreateContext().Refresh(); });
RunTestWithRealm((realm, _) => { realm.Run(r => r.Refresh()); });
}
[Test]
public void TestBlockOperations()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
using (realmFactory.BlockAllOperations())
using (realm.BlockAllOperations())
{
}
});
@ -42,27 +42,26 @@ namespace osu.Game.Tests.Database
[Test]
public void TestNestedContextCreationWithSubscription()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
bool callbackRan = false;
using (var context = realmFactory.CreateContext())
realm.RegisterCustomSubscription(r =>
{
var subscription = context.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
{
using (realmFactory.CreateContext())
realm.Run(_ =>
{
callbackRan = true;
}
});
});
// Force the callback above to run.
using (realmFactory.CreateContext())
{
}
realm.Run(rr => rr.Refresh());
subscription?.Dispose();
}
return null;
});
Assert.IsTrue(callbackRan);
});
@ -71,32 +70,47 @@ namespace osu.Game.Tests.Database
[Test]
public void TestBlockOperationsWithContention()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim();
ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim();
Task.Factory.StartNew(() =>
{
using (realmFactory.CreateContext())
realm.Run(_ =>
{
hasThreadedUsage.Set();
stopThreadedUsage.Wait();
}
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler);
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.CreateContext().Write(r => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realmFactory);
Live<BeatmapInfo> beatmap = realm.Run(r => r.Write(_ => r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()))).ToLive(realm));
ILive<BeatmapInfo> beatmap2 = realmFactory.CreateContext().All<BeatmapInfo>().First().ToLive(realmFactory);
Live<BeatmapInfo> beatmap2 = realm.Run(r => r.All<BeatmapInfo>().First().ToLive(realm));
Assert.AreEqual(beatmap, beatmap2);
});
@ -34,17 +34,22 @@ 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;
Live<BeatmapInfo>? liveBeatmap = null;
using (var context = realmFactory.CreateContext())
realm.Run(r =>
{
context.Write(r => r.Add(beatmap));
r.Write(_ => r.Add(beatmap));
liveBeatmap = beatmap.ToLive(realmFactory);
liveBeatmap = beatmap.ToLive(realm);
});
using (realm.BlockAllOperations())
{
// recycle realm before migrating
}
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
@ -53,7 +58,7 @@ namespace osu.Game.Tests.Database
storage.Migrate(migratedStorage);
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden));
}
});
}
@ -61,14 +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);
using (var context = realmFactory.CreateContext())
context.Write(r => r.Add(beatmap));
realm.Run(r => r.Write(_ => r.Add(beatmap)));
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
});
@ -94,17 +98,17 @@ namespace osu.Game.Tests.Database
[Test]
public void TestScopedReadWithoutContext()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
ILive<BeatmapInfo>? liveBeatmap = null;
Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
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();
Debug.Assert(liveBeatmap != null);
@ -123,17 +127,17 @@ namespace osu.Game.Tests.Database
[Test]
public void TestScopedWriteWithoutContext()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
ILive<BeatmapInfo>? liveBeatmap = null;
Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
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();
Debug.Assert(liveBeatmap != null);
@ -149,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(() =>
{
@ -164,18 +168,18 @@ namespace osu.Game.Tests.Database
[Test]
public void TestValueAccessWithOpenContextFails()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
ILive<BeatmapInfo>? liveBeatmap = null;
Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
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();
Debug.Assert(liveBeatmap != null);
@ -189,13 +193,13 @@ namespace osu.Game.Tests.Database
});
// Can't be used, even from within a valid context.
using (realmFactory.CreateContext())
realm.Run(threadContext =>
{
Assert.Throws<InvalidOperationException>(() =>
{
var __ = liveBeatmap.Value;
});
}
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).WaitSafely();
});
}
@ -203,17 +207,17 @@ namespace osu.Game.Tests.Database
[Test]
public void TestValueAccessWithoutOpenContextFails()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
ILive<BeatmapInfo>? liveBeatmap = null;
Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
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();
Debug.Assert(liveBeatmap != null);
@ -231,54 +235,56 @@ namespace osu.Game.Tests.Database
[Test]
public void TestLiveAssumptions()
{
RunTestWithRealm((realmFactory, _) =>
RunTestWithRealm((realm, _) =>
{
int changesTriggered = 0;
using (var updateThreadContext = realmFactory.CreateContext())
realm.RegisterCustomSubscription(outerRealm =>
{
updateThreadContext.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
ILive<BeatmapInfo>? liveBeatmap = null;
outerRealm.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
Live<BeatmapInfo>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
realm.Run(innerRealm =>
{
var ruleset = CreateRuleset();
var beatmap = threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
var beatmap = innerRealm.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
// add a second beatmap to ensure that a full refresh occurs below.
// not just a refresh from the resolved Live.
threadContext.Write(r => r.Add(new BeatmapInfo(ruleset, new BeatmapDifficulty(), new BeatmapMetadata())));
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();
Debug.Assert(liveBeatmap != null);
// not yet seen by main context
Assert.AreEqual(0, updateThreadContext.All<BeatmapInfo>().Count());
Assert.AreEqual(0, outerRealm.All<BeatmapInfo>().Count());
Assert.AreEqual(0, changesTriggered);
liveBeatmap.PerformRead(resolved =>
{
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
// ReSharper disable once AccessToDisposedClosure
Assert.AreEqual(2, updateThreadContext.All<BeatmapInfo>().Count());
Assert.AreEqual(2, outerRealm.All<BeatmapInfo>().Count());
Assert.AreEqual(1, changesTriggered);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
// ReSharper disable once AccessToDisposedClosure
updateThreadContext.Write(r =>
outerRealm.Write(r =>
{
// can use with the main context.
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,15 +60,12 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer();
// Add some excess bindings for an action which only supports 1.
using (var realm = realmContextFactory.CreateContext())
using (var transaction = realm.BeginWrite())
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)));
transaction.Commit();
}
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));
@ -79,13 +76,13 @@ namespace osu.Game.Tests.Database
private int queryCount(GlobalAction? match = null)
{
using (var realm = realmContextFactory.CreateContext())
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();
}
});
}
[Test]
@ -95,32 +92,32 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
using (var primaryRealm = realmContextFactory.CreateContext())
realm.Run(outerRealm =>
{
var backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
var backBinding = outerRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
var tsr = ThreadSafeReference.Create(backBinding);
using (var threadedContext = realmContextFactory.CreateContext())
realm.Run(innerRealm =>
{
var binding = threadedContext.ResolveReference(tsr);
threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
}
var binding = innerRealm.ResolveReference(tsr);
innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
});
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query.
backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
backBinding = outerRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
});
}
[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.Context.Write(r => r.RemoveAll<BeatmapSetInfo>());
ContextFactory.Context.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 Live<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 Live<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

@ -235,7 +235,7 @@ namespace osu.Game.Tests.Skins.IO
#endregion
private void assertCorrectMetadata(ILive<SkinInfo> import1, string name, string creator, OsuGameBase osu)
private void assertCorrectMetadata(Live<SkinInfo> import1, string name, string creator, OsuGameBase osu)
{
import1.PerformRead(i =>
{
@ -250,7 +250,7 @@ namespace osu.Game.Tests.Skins.IO
});
}
private void assertImportedBoth(ILive<SkinInfo> import1, ILive<SkinInfo> import2)
private void assertImportedBoth(Live<SkinInfo> import1, Live<SkinInfo> import2)
{
import1.PerformRead(i1 => import2.PerformRead(i2 =>
{
@ -260,7 +260,7 @@ namespace osu.Game.Tests.Skins.IO
}));
}
private void assertImportedOnce(ILive<SkinInfo> import1, ILive<SkinInfo> import2)
private void assertImportedOnce(Live<SkinInfo> import1, Live<SkinInfo> import2)
{
import1.PerformRead(i1 => import2.PerformRead(i2 =>
{
@ -334,7 +334,7 @@ namespace osu.Game.Tests.Skins.IO
}
}
private async Task<ILive<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
private async Task<Live<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
{
var skinManager = osu.Dependencies.Get<SkinManager>();
return await skinManager.Import(archive);

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,92 +3,118 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorSaving : OsuGameTestScene
public class TestSceneEditorSaving : EditorSavingTestScene
{
private Editor editor => Game.ChildrenOfType<Editor>().FirstOrDefault();
private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap));
/// <summary>
/// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select.
/// </summary>
[Test]
public void TestNewBeatmapSaveThenLoad()
public void TestMetadata()
{
AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType<MetadataSection>().FirstOrDefault()?.IsLoaded == true);
// We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true);
AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7);
AddStep("Set artist and title", () =>
{
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
EditorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
EditorBeatmap.BeatmapInfo.Metadata.Title = "title";
});
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
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()));
SaveEditor();
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");
AddAssert("Beatmap has correct .osu file path", () => EditorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu");
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct metadata", () => EditorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && EditorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap still has correct author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username == "author");
AddAssert("Beatmap still has correct difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
AddAssert("Beatmap still has correct .osu file path", () => EditorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu");
}
[Test]
public void TestConfiguration()
{
double originalTimelineZoom = 0;
double changedTimelineZoom = 0;
AddStep("Set beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().Value = 16);
AddStep("Set timeline zoom", () =>
{
originalTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom;
var timeline = Editor.ChildrenOfType<Timeline>().Single();
InputManager.MoveMouseTo(timeline);
InputManager.PressKey(Key.AltLeft);
InputManager.ScrollVerticalBy(15f);
InputManager.ReleaseKey(Key.AltLeft);
});
AddAssert("Ensure timeline zoom changed", () =>
{
changedTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom;
return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom);
});
SaveEditor();
AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom);
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom);
}
[Test]
public void TestDifficulty()
{
AddStep("Set overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty = 7);
SaveEditor();
AddAssert("Beatmap has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7);
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct overall difficulty", () => EditorBeatmap.Difficulty.OverallDifficulty == 7);
}
[Test]
public void TestHitObjectPlacement()
{
AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(500, new TimingControlPoint()));
AddStep("Change to placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
checkMutations();
SaveEditor();
AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
// After placement these must be non-default as defaults are read-only.
AddAssert("Placed object has non-default control points", () =>
editorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT &&
editorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT);
EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT &&
EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT);
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
ReloadEditorToSameBeatmap();
checkMutations();
AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new PlaySongSelect());
AddUntilStep("wait for carousel load", () => songSelect.BeatmapSetsLoaded);
AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
checkMutations();
}
private void checkMutations()
{
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
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 difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
// After placement these must be non-default as defaults are read-only.
AddAssert("Placed object still has non-default control points", () =>
EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT &&
EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT);
}
}
}

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

@ -47,25 +47,25 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) },
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 1000, Position = new Vector2(100) },
new HitCircle { StartTime = 1500, Position = new Vector2(200) },
new HitCircle { StartTime = 2000, Position = new Vector2(300) },
}));
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
AddStep("nudge forwards", () => InputManager.Key(Key.K));
AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 100);
AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 500);
AddStep("nudge backwards", () => InputManager.Key(Key.J));
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 500);
}
[Test]
public void TestBasicSelect()
{
var addedObject = new HitCircle { StartTime = 100 };
var addedObject = new HitCircle { StartTime = 500 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
moveMouseToObject(() => addedObject);
@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Editing
var addedObject2 = new HitCircle
{
StartTime = 200,
StartTime = 1000,
Position = new Vector2(100),
};
@ -92,10 +92,10 @@ namespace osu.Game.Tests.Visual.Editing
{
var addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) },
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 1000, Position = new Vector2(100) },
new HitCircle { StartTime = 1500, Position = new Vector2(200) },
new HitCircle { StartTime = 2000, Position = new Vector2(300) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
@ -125,7 +125,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestBasicDeselect()
{
var addedObject = new HitCircle { StartTime = 100 };
var addedObject = new HitCircle { StartTime = 500 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
moveMouseToObject(() => addedObject);
@ -166,11 +166,11 @@ namespace osu.Game.Tests.Visual.Editing
{
var addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) },
new HitCircle { StartTime = 500, Position = new Vector2(400) },
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 1000, Position = new Vector2(100) },
new HitCircle { StartTime = 1500, Position = new Vector2(200) },
new HitCircle { StartTime = 2000, Position = new Vector2(300) },
new HitCircle { StartTime = 2500, Position = new Vector2(400) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
@ -236,10 +236,10 @@ namespace osu.Game.Tests.Visual.Editing
{
var addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) },
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 1000, Position = new Vector2(100) },
new HitCircle { StartTime = 1500, Position = new Vector2(200) },
new HitCircle { StartTime = 2000, Position = new Vector2(300) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));

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

@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestScoreImportThenDelete()
{
ILive<ScoreInfo> imported = null;
Live<ScoreInfo> imported = null;
AddStep("create button without replay", () =>
{
@ -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]
@ -154,11 +153,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestDownloadButtonHiddenWhenBeatmapExists()
{
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
ILive<BeatmapSetInfo> imported = null;
Live<BeatmapSetInfo> imported = null;
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

@ -50,10 +50,20 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("close settings", () => Game.Settings.Hide());
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
PushAndConfirm(() => new PlaySongSelect());
AddUntilStep("wait for selection", () => !Game.Beatmap.IsDefault);
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () =>
{
// dismiss any notifications that may appear (ie. muted notification).
clickMouseInCentre();
return player != null;
});
AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false);
AddStep("press 'z'", () => InputManager.Key(Key.Z));
@ -63,6 +73,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("key counter did increase", () => keyCounter.CountPresses == 1);
}
private void clickMouseInCentre()
{
InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
}
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel
.ChildrenOfType<VariantBindingsSubsection>()
.FirstOrDefault(s => s.Ruleset.ShortName == "osu");
@ -76,7 +92,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,21 +36,21 @@ 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();
using (var realm = realmContextFactory.CreateContext())
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);
}
});
}
[Test]

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());
@ -65,13 +64,13 @@ namespace osu.Game.Tests.Visual.SongSelect
{
base.SetUpSteps();
AddStep("delete all beatmaps", () =>
AddStep("reset defaults", () =>
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
manager?.Delete(manager.GetAllUsableBeatmapSets());
Beatmap.SetDefault();
});
AddStep("delete all beatmaps", () => manager?.Delete());
}
[Test]
@ -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,11 +122,11 @@ namespace osu.Game.Tests.Visual.UserInterface
[SetUp]
public void Setup() => Schedule(() =>
{
using (var realm = realmFactory.CreateContext())
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;
leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables

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

@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Components
if (manager == null)
{
AddInternal(manager = new ChannelManager());
AddInternal(manager = new ChannelManager { HighPollRate = { Value = true } });
Channel.BindTo(manager.CurrentChannel);
}

View File

@ -111,7 +111,7 @@ namespace osu.Game.Beatmaps
public int GridSize { get; set; }
public double TimelineZoom { get; set; }
public double TimelineZoom { get; set; } = 1.0;
[Ignored]
public CountdownType Countdown { get; set; } = CountdownType.Normal;

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,15 +118,17 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
public void Hide(BeatmapInfo beatmapInfo)
{
using (var realm = contextFactory.CreateContext())
using (var transaction = realm.BeginWrite())
realm.Run(r =>
{
if (!beatmapInfo.IsManaged)
beatmapInfo = realm.Find<BeatmapInfo>(beatmapInfo.ID);
using (var transaction = r.BeginWrite())
{
if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
beatmapInfo.Hidden = true;
transaction.Commit();
}
beatmapInfo.Hidden = true;
transaction.Commit();
}
});
}
/// <summary>
@ -136,27 +137,31 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapInfo">The beatmap difficulty to restore.</param>
public void Restore(BeatmapInfo beatmapInfo)
{
using (var realm = contextFactory.CreateContext())
using (var transaction = realm.BeginWrite())
realm.Run(r =>
{
if (!beatmapInfo.IsManaged)
beatmapInfo = realm.Find<BeatmapInfo>(beatmapInfo.ID);
using (var transaction = r.BeginWrite())
{
if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
beatmapInfo.Hidden = false;
transaction.Commit();
}
beatmapInfo.Hidden = false;
transaction.Commit();
}
});
}
public void RestoreAll()
{
using (var realm = contextFactory.CreateContext())
using (var transaction = realm.BeginWrite())
realm.Run(r =>
{
foreach (var beatmap in realm.All<BeatmapInfo>().Where(b => b.Hidden))
beatmap.Hidden = false;
using (var transaction = r.BeginWrite())
{
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Hidden))
beatmap.Hidden = false;
transaction.Commit();
}
transaction.Commit();
}
});
}
/// <summary>
@ -165,8 +170,11 @@ namespace osu.Game.Beatmaps
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
{
using (var context = contextFactory.CreateContext())
return context.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
return realm.Run(r =>
{
r.Refresh();
return r.All<BeatmapSetInfo>().Where(b => !b.DeletePending).Detach();
});
}
/// <summary>
@ -174,10 +182,9 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="query">The query.</param>
/// <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)
public Live<BeatmapSetInfo>? QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query)
{
using (var context = contextFactory.CreateContext())
return context.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).
@ -232,21 +239,20 @@ namespace osu.Game.Beatmaps
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
{
using (var context = contextFactory.CreateContext())
realm.Run(r =>
{
var items = context.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);
beatmapModelManager.Delete(items.ToList(), silent);
}
});
}
public void UndeleteAll()
{
using (var context = contextFactory.CreateContext())
beatmapModelManager.Undelete(context.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)
@ -273,22 +279,22 @@ namespace osu.Game.Beatmaps
return beatmapModelManager.Import(tasks);
}
public Task<IEnumerable<ILive<BeatmapSetInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks)
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks)
{
return beatmapModelManager.Import(notification, tasks);
}
public Task<ILive<BeatmapSetInfo>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return beatmapModelManager.Import(task, lowPriority, cancellationToken);
}
public Task<ILive<BeatmapSetInfo>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
public Task<Live<BeatmapSetInfo>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return beatmapModelManager.Import(archive, lowPriority, cancellationToken);
}
public Task<ILive<BeatmapSetInfo>?> Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken);
}
@ -305,19 +311,19 @@ 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)
{
using (var realm = contextFactory.CreateContext())
realm.Run(r =>
{
var refetch = realm.Find<BeatmapInfo>(importedBeatmap.ID)?.Detach();
var refetch = r.Find<BeatmapInfo>(importedBeatmap.ID)?.Detach();
if (refetch != null)
importedBeatmap = refetch;
}
});
}
return workingBeatmapCache.GetWorkingBeatmap(importedBeatmap);
}
public WorkingBeatmap GetWorkingBeatmap(ILive<BeatmapInfo>? importedBeatmap)
public WorkingBeatmap GetWorkingBeatmap(Live<BeatmapInfo>? importedBeatmap)
{
WorkingBeatmap working = workingBeatmapCache.GetWorkingBeatmap(null);
@ -361,7 +367,7 @@ namespace osu.Game.Beatmaps
#region Implementation of IPostImports<out BeatmapSetInfo>
public Action<IEnumerable<ILive<BeatmapSetInfo>>>? PostImport
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PostImport
{
set => beatmapModelManager.PostImport = value;
}

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,17 +98,16 @@ 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)
{
using (var context = ContextFactory.CreateContext())
return context.All<BeatmapInfo>().FirstOrDefault(query)?.Detach();
return Realm.Run(realm => realm.All<BeatmapInfo>().FirstOrDefault(query)?.Detach());
}
public void Update(BeatmapSetInfo item)
{
using (var realm = ContextFactory.CreateContext())
Realm.Write(realm =>
{
var existing = realm.Find<BeatmapSetInfo>(item.ID);
realm.Write(r => item.CopyChangesToRealm(existing));
}
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

@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.EntityFrameworkCore.Storage;
using osu.Framework.Development;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
@ -151,9 +150,6 @@ namespace osu.Game.Database
{
Logger.Log($"Creating full EF database backup at {backupFilename}", LoggingTarget.Database);
if (DebugUtils.IsDebugBuild)
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);
using (var source = storage.GetStream(DATABASE_NAME))
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
source.CopyTo(destination);

View File

@ -1,55 +1,140 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK;
using Realms;
#nullable enable
namespace osu.Game.Database
{
internal class EFToRealmMigrator
internal class EFToRealmMigrator : CompositeDrawable
{
private readonly DatabaseContextFactory efContextFactory;
private readonly RealmContextFactory realmContextFactory;
private readonly OsuConfigManager config;
private readonly Storage storage;
public Task<bool> MigrationCompleted => migrationCompleted.Task;
public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config, Storage storage)
private readonly TaskCompletionSource<bool> migrationCompleted = new TaskCompletionSource<bool>();
[Resolved]
private DatabaseContextFactory efContextFactory { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private readonly OsuSpriteText currentOperationText;
public EFToRealmMigrator()
{
this.efContextFactory = efContextFactory;
this.realmContextFactory = realmContextFactory;
this.config = config;
this.storage = storage;
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(10),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Database migration in progress",
Font = OsuFont.Default.With(size: 40)
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "This could take a few minutes depending on the speed of your disk(s).",
Font = OsuFont.Default.With(size: 30)
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Please keep the window open until this completes!",
Font = OsuFont.Default.With(size: 30)
},
new LoadingSpinner(true)
{
State = { Value = Visibility.Visible }
},
currentOperationText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 30)
},
}
},
};
}
public void Run()
protected override void LoadComplete()
{
createBackup();
base.LoadComplete();
using (var ef = efContextFactory.Get())
Task.Factory.StartNew(() =>
{
migrateSettings(ef);
migrateSkins(ef);
migrateBeatmaps(ef);
migrateScores(ef);
}
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>();
});
// Delete the database permanently.
// Will cause future startups to not attempt migration.
Logger.Log("Migration successful, deleting EF database", LoggingTarget.Database);
efContextFactory.ResetDatabase();
migrateSettings(ef);
migrateSkins(ef);
migrateBeatmaps(ef);
migrateScores(ef);
}
// Delete the database permanently.
// Will cause future startups to not attempt migration.
log("Migration successful, deleting EF database");
efContextFactory.ResetDatabase();
if (DebugUtils.IsDebugBuild)
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 =>
{
migrationCompleted.SetResult(true);
});
}
private void log(string message)
{
Logger.Log(message, LoggingTarget.Database);
Scheduler.AddOnce(m => currentOperationText.Text = m, message);
}
private void migrateBeatmaps(OsuDbContext ef)
@ -62,103 +147,94 @@ namespace osu.Game.Database
.Include(s => s.Files).ThenInclude(f => f.FileInfo)
.Include(s => s.Metadata);
Logger.Log("Beginning beatmaps migration to realm", LoggingTarget.Database);
log("Beginning beatmaps migration to realm");
// previous entries in EF are removed post migration.
if (!existingBeatmapSets.Any())
{
Logger.Log("No beatmaps found to migrate", LoggingTarget.Database);
log("No beatmaps found to migrate");
return;
}
int count = existingBeatmapSets.Count();
using (var realm = realmContextFactory.CreateContext())
realm.Run(r =>
{
Logger.Log($"Found {count} beatmaps in EF", LoggingTarget.Database);
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))
{
Logger.Log("Skipping migration as realm already has beatmaps loaded", LoggingTarget.Database);
}
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();
Logger.Log($"Migrated {written}/{count} beatmaps...", LoggingTarget.Database);
}
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();
}
Logger.Log($"Successfully migrated {count} beatmaps to realm", LoggingTarget.Database);
r.Add(realmBeatmapSet);
}
}
}
finally
{
transaction.Commit();
}
log($"Successfully migrated {count} beatmaps to realm");
});
}
private BeatmapMetadata getBestMetadata(EFBeatmapMetadata? beatmapMetadata, EFBeatmapMetadata? beatmapSetMetadata)
@ -193,86 +269,78 @@ namespace osu.Game.Database
.Include(s => s.Files)
.ThenInclude(f => f.FileInfo);
Logger.Log("Beginning scores migration to realm", LoggingTarget.Database);
log("Beginning scores migration to realm");
// previous entries in EF are removed post migration.
if (!existingScores.Any())
{
Logger.Log("No scores found to migrate", LoggingTarget.Database);
log("No scores found to migrate");
return;
}
int count = existingScores.Count();
using (var realm = realmContextFactory.CreateContext())
realm.Run(r =>
{
Logger.Log($"Found {count} scores in EF", LoggingTarget.Database);
log($"Found {count} scores in EF");
// only migrate data if the realm database is empty.
if (realm.All<ScoreInfo>().Any())
{
Logger.Log("Skipping migration as realm already has scores loaded", LoggingTarget.Database);
}
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();
Logger.Log($"Migrated {written}/{count} scores...", LoggingTarget.Database);
}
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();
}
Logger.Log($"Successfully migrated {count} scores to realm", LoggingTarget.Database);
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");
});
}
private void migrateSkins(OsuDbContext db)
@ -301,37 +369,39 @@ namespace osu.Game.Database
break;
}
using (var realm = realmContextFactory.CreateContext())
using (var transaction = realm.BeginWrite())
realm.Run(r =>
{
// 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))
using (var transaction = r.BeginWrite())
{
Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database);
foreach (var skin in existingSkins)
// only migrate data if the realm database is empty.
// 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))
{
var realmSkin = new SkinInfo
log($"Migrating {existingSkins.Count} skins");
foreach (var skin in existingSkins)
{
Name = skin.Name,
Creator = skin.Creator,
Hash = skin.Hash,
Protected = false,
InstantiationInfo = skin.InstantiationInfo,
};
var realmSkin = new SkinInfo
{
Name = skin.Name,
Creator = skin.Creator,
Hash = skin.Hash,
Protected = false,
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();
if (skin.ID == userSkinInt)
userSkinChoice.Value = realmSkin.ID.ToString();
}
}
}
transaction.Commit();
}
transaction.Commit();
}
});
}
private static void migrateFiles<T>(IHasFiles<T> fileSource, Realm realm, IHasRealmFiles realmObject) where T : INamedFileInfo
@ -356,53 +426,43 @@ namespace osu.Game.Database
if (!existingSettings.Any())
return;
Logger.Log("Beginning settings migration to realm", LoggingTarget.Database);
log("Beginning settings migration to realm");
using (var realm = realmContextFactory.CreateContext())
using (var transaction = realm.BeginWrite())
realm.Run(r =>
{
// only migrate data if the realm database is empty.
if (!realm.All<RealmRulesetSetting>().Any())
using (var transaction = r.BeginWrite())
{
Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database);
foreach (var dkb in existingSettings)
// only migrate data if the realm database is empty.
if (!r.All<RealmRulesetSetting>().Any())
{
if (dkb.RulesetID == null)
continue;
log($"Migrating {existingSettings.Count} settings");
string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value);
if (string.IsNullOrEmpty(shortName))
continue;
realm.Add(new RealmRulesetSetting
foreach (var dkb in existingSettings)
{
Key = dkb.Key,
Value = dkb.StringValue,
RulesetName = shortName,
Variant = dkb.Variant ?? 0,
});
}
}
if (dkb.RulesetID == null)
continue;
transaction.Commit();
}
string? shortName = getRulesetShortNameFromLegacyID(dkb.RulesetID.Value);
if (string.IsNullOrEmpty(shortName))
continue;
r.Add(new RealmRulesetSetting
{
Key = dkb.Key,
Value = dkb.StringValue,
RulesetName = shortName,
Variant = dkb.Variant ?? 0,
});
}
}
transaction.Commit();
}
});
}
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
private void createBackup()
{
string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
efContextFactory.CreateBackup($"client.{migration}.db");
realmContextFactory.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);
}
}
}

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

@ -16,9 +16,9 @@ namespace osu.Game.Database
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>, ICanAcceptFiles
where TModel : class
where TModel : class, IHasGuidPrimaryKey
{
Task<IEnumerable<ILive<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
/// <summary>
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
@ -28,7 +28,7 @@ namespace osu.Game.Database
/// <param name="lowPriority">Whether this is a low priority import.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The imported model, if successful.</returns>
Task<ILive<TModel>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default);
Task<Live<TModel>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default);
/// <summary>
/// Silently import an item from an <see cref="ArchiveReader"/>.
@ -36,7 +36,7 @@ namespace osu.Game.Database
/// <param name="archive">The archive to be imported.</param>
/// <param name="lowPriority">Whether this is a low priority import.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
Task<ILive<TModel>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default);
Task<Live<TModel>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default);
/// <summary>
/// Silently import an item from a <typeparamref name="TModel"/>.
@ -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);
Live<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

@ -9,11 +9,11 @@ using System.Collections.Generic;
namespace osu.Game.Database
{
public interface IPostImports<TModel>
where TModel : class
where TModel : class, IHasGuidPrimaryKey
{
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<ILive<TModel>>>? PostImport { set; }
public Action<IEnumerable<Live<TModel>>>? PostImport { set; }
}
}

View File

@ -1,20 +0,0 @@
// 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 Realms;
namespace osu.Game.Database
{
public interface IRealmFactory
{
/// <summary>
/// The main realm context, bound to the update thread.
/// </summary>
Realm Context { get; }
/// <summary>
/// Create a new realm context for use on the current thread.
/// </summary>
Realm CreateContext();
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Database
/// A class which handles importing legacy user data of a single type from osu-stable.
/// </summary>
public abstract class LegacyModelImporter<TModel>
where TModel : class
where TModel : class, IHasGuidPrimaryKey
{
/// <summary>
/// The relative path from osu-stable's data directory to import items from.

View File

@ -3,39 +3,41 @@
using System;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// A wrapper to provide access to database backed classes in a thread-safe manner.
/// </summary>
/// <typeparam name="T">The databased type.</typeparam>
public interface ILive<T> : IEquatable<ILive<T>>
where T : class // TODO: Add IHasGuidPrimaryKey once we don't need EF support any more.
public abstract class Live<T> : IEquatable<Live<T>>
where T : class, IHasGuidPrimaryKey
{
Guid ID { get; }
public Guid ID { get; }
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
void PerformRead(Action<T> perform);
public abstract void PerformRead(Action<T> perform);
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
TReturn PerformRead<TReturn>(Func<T, TReturn> perform);
public abstract TReturn PerformRead<TReturn>(Func<T, TReturn> perform);
/// <summary>
/// Perform a write operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
void PerformWrite(Action<T> perform);
public abstract void PerformWrite(Action<T> perform);
/// <summary>
/// Whether this instance is tracking data which is managed by the database backing.
/// </summary>
bool IsManaged { get; }
public abstract bool IsManaged { get; }
/// <summary>
/// Resolve the value of this instance on the update thread.
@ -43,6 +45,15 @@ namespace osu.Game.Database
/// <remarks>
/// After resolving, the data should not be passed between threads.
/// </remarks>
T Value { get; }
public abstract T Value { get; }
protected Live(Guid id)
{
ID = id;
}
public bool Equals(Live<T>? other) => ID == other?.ID;
public override string ToString() => PerformRead(i => i.ToString());
}
}

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, IRealmFactory
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(CreateContext)} 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);
@ -169,29 +200,187 @@ namespace osu.Game.Database
/// <returns></returns>
public bool Compact() => Realm.Compact(getConfiguration());
public Realm CreateContext()
/// <summary>
/// Run work on realm with a return value.
/// </summary>
/// <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(Realm);
using (var realm = getRealmInstance())
return action(realm);
}
/// <summary>
/// Run work on realm.
/// </summary>
/// <param name="action">The work to run.</param>
public void Run(Action<Realm> action)
{
if (ThreadSafety.IsUpdateThread)
action(Realm);
else
{
using (var realm = getRealmInstance())
action(realm);
}
}
/// <summary>
/// Write changes to realm.
/// </summary>
/// <param name="action">The work to run.</param>
public void Write(Action<Realm> action)
{
if (ThreadSafety.IsUpdateThread)
Realm.Write(action);
else
{
using (var realm = getRealmInstance())
realm.Write(action);
}
}
/// <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());
}
@ -199,8 +388,8 @@ namespace osu.Game.Database
{
if (tookSemaphoreLock)
{
contextCreationLock.Release();
currentThreadCanCreateContexts.Value = false;
realmRetrievalLock.Release();
currentThreadCanCreateRealmInstances.Value = false;
}
}
}
@ -389,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.
@ -399,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;
@ -440,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
@ -458,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

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Development;
using osu.Framework.Statistics;
using Realms;
#nullable enable
@ -13,37 +15,38 @@ namespace osu.Game.Database
/// Provides a method of working with realm objects over longer application lifetimes.
/// </summary>
/// <typeparam name="T">The underlying object type.</typeparam>
public class RealmLive<T> : ILive<T> where T : RealmObject, IHasGuidPrimaryKey
public class RealmLive<T> : Live<T> where T : RealmObject, IHasGuidPrimaryKey
{
public Guid ID { get; }
public bool IsManaged => data.IsManaged;
public override bool IsManaged => data.IsManaged;
/// <summary>
/// The original live data used to create this instance.
/// </summary>
private readonly T data;
private T data;
private readonly RealmContextFactory realmFactory;
private bool dataIsFromUpdateThread;
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)
: base(data.ID)
{
this.data = data;
this.realmFactory = realmFactory;
this.realm = realm;
ID = data.ID;
dataIsFromUpdateThread = ThreadSafety.IsUpdateThread;
}
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public void PerformRead(Action<T> perform)
public override void PerformRead(Action<T> perform)
{
if (!IsManaged)
{
@ -51,35 +54,52 @@ namespace osu.Game.Database
return;
}
using (var realm = realmFactory.CreateContext())
perform(realm.Find<T>(ID));
realm.Run(r =>
{
if (ThreadSafety.IsUpdateThread)
{
ensureDataIsFromUpdateThread();
perform(data);
return;
}
perform(retrieveFromID(r, ID));
RealmLiveStatistics.USAGE_ASYNC.Value++;
});
}
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public TReturn PerformRead<TReturn>(Func<T, TReturn> perform)
public override TReturn PerformRead<TReturn>(Func<T, TReturn> perform)
{
if (!IsManaged)
return perform(data);
using (var realm = realmFactory.CreateContext())
if (ThreadSafety.IsUpdateThread)
{
var returnData = perform(realm.Find<T>(ID));
ensureDataIsFromUpdateThread();
return perform(data);
}
return realm.Run(r =>
{
var returnData = perform(retrieveFromID(r, ID));
RealmLiveStatistics.USAGE_ASYNC.Value++;
if (returnData is RealmObjectBase realmObject && realmObject.IsManaged)
throw new InvalidOperationException(@$"Managed realm objects should not exit the scope of {nameof(PerformRead)}.");
return returnData;
}
});
}
/// <summary>
/// Perform a write operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public void PerformWrite(Action<T> perform)
public override void PerformWrite(Action<T> perform)
{
if (!IsManaged)
throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value");
@ -89,10 +109,11 @@ namespace osu.Game.Database
var transaction = t.Realm.BeginWrite();
perform(t);
transaction.Commit();
RealmLiveStatistics.WRITES.Value++;
});
}
public T Value
public override T Value
{
get
{
@ -102,12 +123,48 @@ 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);
ensureDataIsFromUpdateThread();
return data;
}
}
public bool Equals(ILive<T>? other) => ID == other?.ID;
private void ensureDataIsFromUpdateThread()
{
Debug.Assert(ThreadSafety.IsUpdateThread);
public override string ToString() => PerformRead(i => i.ToString());
if (dataIsFromUpdateThread && !data.Realm.IsClosed)
{
RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++;
return;
}
dataIsFromUpdateThread = true;
data = retrieveFromID(realm.Realm, ID);
RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++;
}
private T retrieveFromID(Realm realm, Guid id)
{
var found = realm.Find<T>(ID);
if (found == null)
{
// It may be that we access this from the update thread before a refresh has taken place.
// To ensure that behaviour matches what we'd expect (the object *is* available), force
// a refresh to bring in any off-thread changes immediately.
realm.Refresh();
found = realm.Find<T>(ID);
}
return found;
}
}
internal static class RealmLiveStatistics
{
public static readonly GlobalStatistic<int> WRITES = GlobalStatistics.Get<int>(@"Realm", @"Live writes");
public static readonly GlobalStatistic<int> USAGE_UPDATE_IMMEDIATE = GlobalStatistics.Get<int>(@"Realm", @"Live update read (fast)");
public static readonly GlobalStatistic<int> USAGE_UPDATE_REFETCH = GlobalStatistics.Get<int>(@"Realm", @"Live update read (slow)");
public static readonly GlobalStatistic<int> USAGE_ASYNC = GlobalStatistics.Get<int>(@"Realm", @"Live async read");
}
}

View File

@ -13,34 +13,32 @@ namespace osu.Game.Database
/// Usually used for testing purposes where the instance is never required to be managed.
/// </summary>
/// <typeparam name="T">The underlying object type.</typeparam>
public class RealmLiveUnmanaged<T> : ILive<T> where T : RealmObjectBase, IHasGuidPrimaryKey
public class RealmLiveUnmanaged<T> : Live<T> where T : RealmObjectBase, IHasGuidPrimaryKey
{
/// <summary>
/// The original live data used to create this instance.
/// </summary>
public override T Value { get; }
/// <summary>
/// Construct a new instance of live realm data.
/// </summary>
/// <param name="data">The realm data.</param>
public RealmLiveUnmanaged(T data)
: base(data.ID)
{
if (data.IsManaged)
throw new InvalidOperationException($"Cannot use {nameof(RealmLiveUnmanaged<T>)} with managed instances");
Value = data;
}
public bool Equals(ILive<T>? other) => ID == other?.ID;
public override void PerformRead(Action<T> perform) => perform(Value);
public override string ToString() => Value.ToString();
public override TReturn PerformRead<TReturn>(Func<T, TReturn> perform) => perform(Value);
public Guid ID => Value.ID;
public override void PerformWrite(Action<T> perform) => throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value");
public void PerformRead(Action<T> perform) => perform(Value);
public TReturn PerformRead<TReturn>(Func<T, TReturn> perform) => perform(Value);
public void PerformWrite(Action<T> perform) => throw new InvalidOperationException(@"Can't perform writes on a non-managed underlying value");
public bool IsManaged => false;
/// <summary>
/// The original live data used to create this instance.
/// </summary>
public T Value { get; }
public override bool IsManaged => false;
}
}

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;
@ -205,28 +204,28 @@ namespace osu.Game.Database
private static void copyChangesToRealm<T>(T source, T destination) where T : RealmObjectBase
=> write_mapper.Map(source, destination);
public static List<ILive<T>> ToLiveUnmanaged<T>(this IEnumerable<T> realmList)
public static List<Live<T>> ToLiveUnmanaged<T>(this IEnumerable<T> realmList)
where T : RealmObject, IHasGuidPrimaryKey
{
return realmList.Select(l => new RealmLiveUnmanaged<T>(l)).Cast<ILive<T>>().ToList();
return realmList.Select(l => new RealmLiveUnmanaged<T>(l)).Cast<Live<T>>().ToList();
}
public static ILive<T> ToLiveUnmanaged<T>(this T realmObject)
public static Live<T> ToLiveUnmanaged<T>(this T realmObject)
where T : RealmObject, IHasGuidPrimaryKey
{
return new RealmLiveUnmanaged<T>(realmObject);
}
public static List<ILive<T>> ToLive<T>(this IEnumerable<T> realmList, RealmContextFactory realmContextFactory)
public static List<Live<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<Live<T>>().ToList();
}
public static ILive<T> ToLive<T>(this T realmObject, RealmContextFactory realmContextFactory)
public static Live<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>();
using (var context = realmFactory.CreateContext())
realm.Run(context =>
{
foreach (var action in context.All<RealmKeyBinding>().Where(b => string.IsNullOrEmpty(b.RulesetName) && (GlobalAction)b.ActionInt == globalAction))
{
@ -44,7 +44,7 @@ namespace osu.Game.Input
if (str.Length > 0)
combinations.Add(str);
}
}
});
return combinations;
}
@ -56,24 +56,26 @@ namespace osu.Game.Input
/// <param name="rulesets">The rulesets to populate defaults from.</param>
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
{
using (var realm = realmFactory.CreateContext())
using (var transaction = realm.BeginWrite())
realm.Run(r =>
{
// 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();
insertDefaults(realm, existingBindings, container.DefaultKeyBindings);
foreach (var ruleset in rulesets)
using (var transaction = r.BeginWrite())
{
var instance = ruleset.CreateInstance();
foreach (int variant in instance.AvailableVariants)
insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant);
}
// 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 = r.All<RealmKeyBinding>().ToList();
transaction.Commit();
}
insertDefaults(r, existingBindings, container.DefaultKeyBindings);
foreach (var ruleset in rulesets)
{
var instance = ruleset.CreateInstance();
foreach (int variant in instance.AvailableVariants)
insertDefaults(r, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ShortName, variant);
}
transaction.Commit();
}
});
}
private void insertDefaults(Realm realm, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, string? rulesetName = null, int? variant = null)

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

@ -249,7 +249,7 @@ namespace osu.Game
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
configSkin.ValueChanged += skinId =>
{
ILive<SkinInfo> skinInfo = null;
Live<SkinInfo> skinInfo = null;
if (Guid.TryParse(skinId.NewValue, out var guid))
skinInfo = SkinManager.Query(s => s.ID == guid);
@ -439,7 +439,7 @@ namespace osu.Game
/// </remarks>
public void PresentBeatmap(IBeatmapSetInfo beatmap, Predicate<BeatmapInfo> difficultyCriteria = null)
{
ILive<BeatmapSetInfo> databasedSet = null;
Live<BeatmapSetInfo> databasedSet = null;
if (beatmap.OnlineID > 0)
databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID);

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