diff --git a/osu.Android.props b/osu.Android.props
index 398c1d91dd..8c15ed7949 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
index ab14f939d4..60489c1b22 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
@@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
base.LoadComplete();
- complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
+ complete.BindValueChanged(complete => updateDiscColour(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
@@ -137,6 +137,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
this.ScaleTo(initial_scale);
this.RotateTo(0);
+ updateDiscColour(false);
+
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
@@ -177,12 +179,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
- // transforms we have from completing the spinner will be rolled back, so reapply immediately.
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
- updateComplete(state == ArmedState.Hit, 0);
+ if (drawableSpinner.Result?.TimeCompleted is double completionTime)
+ {
+ using (BeginAbsoluteSequence(completionTime))
+ updateDiscColour(true, 200);
+ }
}
- private void updateComplete(bool complete, double duration)
+ private void updateDiscColour(bool complete, double duration = 0)
{
var colour = complete ? completeColour : normalColour;
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
index ad55e19a13..436d71fda2 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -196,6 +196,27 @@ namespace osu.Game.Rulesets.Osu.Utils
var slider = (Slider)workingObject.HitObject;
var possibleMovementBounds = CalculatePossibleMovementBounds(slider);
+ // The slider rotation applied in computeModifiedPosition might make it impossible to fit the slider into the playfield
+ // For example, a long horizontal slider will be off-screen when rotated by 90 degrees
+ // In this case, limit the rotation to either 0 or 180 degrees
+ if (possibleMovementBounds.Width < 0 || possibleMovementBounds.Height < 0)
+ {
+ float currentRotation = getSliderRotation(slider);
+ float diff1 = getAngleDifference(workingObject.RotationOriginal, currentRotation);
+ float diff2 = getAngleDifference(workingObject.RotationOriginal + MathF.PI, currentRotation);
+
+ if (diff1 < diff2)
+ {
+ RotateSlider(slider, workingObject.RotationOriginal - getSliderRotation(slider));
+ }
+ else
+ {
+ RotateSlider(slider, workingObject.RotationOriginal + MathF.PI - getSliderRotation(slider));
+ }
+
+ possibleMovementBounds = calculatePossibleMovementBounds(slider);
+ }
+
var previousPosition = workingObject.PositionModified;
// Clamp slider position to the placement area
@@ -355,6 +376,18 @@ namespace osu.Game.Rulesets.Osu.Utils
return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
}
+ ///
+ /// Get the absolute difference between 2 angles measured in Radians.
+ ///
+ /// The first angle
+ /// The second angle
+ /// The absolute difference with interval [0, MathF.PI)
+ private static float getAngleDifference(float angle1, float angle2)
+ {
+ float diff = MathF.Abs(angle1 - angle2) % (MathF.PI * 2);
+ return MathF.Min(diff, MathF.PI * 2 - diff);
+ }
+
public class ObjectPositionInfo
{
///
@@ -397,6 +430,7 @@ namespace osu.Game.Rulesets.Osu.Utils
private class WorkingObject
{
+ public float RotationOriginal { get; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionModified { get; set; }
public Vector2 EndPositionModified { get; set; }
@@ -407,6 +441,7 @@ namespace osu.Game.Rulesets.Osu.Utils
public WorkingObject(ObjectPositionInfo positionInfo)
{
PositionInfo = positionInfo;
+ RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
PositionModified = PositionOriginal = HitObject.Position;
EndPositionModified = HitObject.EndPosition;
}
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
index a50eb22c67..3615cebe6a 100644
--- a/osu.Game.Tests/Database/RealmLiveTests.cs
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -59,6 +59,64 @@ namespace osu.Game.Tests.Database
});
}
+ [Test]
+ public void TestFailedWritePerformsRollback()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ Assert.Throws(() =>
+ {
+ realm.Write(r =>
+ {
+ r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()));
+ throw new InvalidOperationException();
+ });
+ });
+
+ Assert.That(realm.Run(r => r.All()), Is.Empty);
+ });
+ }
+
+ [Test]
+ public void TestFailedNestedWritePerformsRollback()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ Assert.Throws(() =>
+ {
+ realm.Write(r =>
+ {
+ realm.Write(_ =>
+ {
+ r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()));
+ throw new InvalidOperationException();
+ });
+ });
+ });
+
+ Assert.That(realm.Run(r => r.All()), Is.Empty);
+ });
+ }
+
+ [Test]
+ public void TestNestedWriteCalls()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
+
+ var liveBeatmap = beatmap.ToLive(realm);
+
+ realm.Run(r =>
+ r.Write(_ =>
+ r.Write(_ =>
+ r.Add(beatmap)))
+ );
+
+ Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
+ });
+ }
+
[Test]
public void TestAccessAfterAttach()
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
index ae431e77ae..0395ae9d99 100644
--- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
@@ -9,7 +9,6 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Framework.Timing;
-using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
@@ -116,10 +115,10 @@ namespace osu.Game.Tests.Gameplay
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
- AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
+ AddStep("gameplay clock time = 2500", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 2500, 10f));
AddStep("seek to 10000", () => gameplayClockContainer.Seek(10000));
- AddAssert("gameplay clock time = 10000", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 10000, 10f));
+ AddStep("gameplay clock time = 10000", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 10000, 10f));
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 0bf47141e4..fcf69bf6f2 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -212,17 +212,17 @@ namespace osu.Game.Tests.Online
{
}
- protected override BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapUpdater beatmapUpdater)
+ protected override BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm)
{
- return new TestBeatmapImporter(this, storage, realm, beatmapUpdater);
+ return new TestBeatmapImporter(this, storage, realm);
}
internal class TestBeatmapImporter : BeatmapImporter
{
private readonly TestBeatmapManager testBeatmapManager;
- public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapUpdater beatmapUpdater)
- : base(storage, databaseAccess, beatmapUpdater)
+ public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess)
+ : base(storage, databaseAccess)
{
this.testBeatmapManager = testBeatmapManager;
}
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index 8b7fcae1a9..c3c10215a5 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -83,20 +83,20 @@ namespace osu.Game.Tests.Skins.IO
#region Cases where imports should match existing
[Test]
- public Task TestImportTwiceWithSameMetadataAndFilename() => runSkinTest(async osu =>
+ public Task TestImportTwiceWithSameMetadataAndFilename([Values] bool batchImport) => runSkinTest(async osu =>
{
- var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
- var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
+ var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"), batchImport);
+ var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"), batchImport);
assertImportedOnce(import1, import2);
});
[Test]
- public Task TestImportTwiceWithNoMetadataSameDownloadFilename() => runSkinTest(async osu =>
+ public Task TestImportTwiceWithNoMetadataSameDownloadFilename([Values] bool batchImport) => runSkinTest(async osu =>
{
// if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety.
- var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"));
- var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"));
+ var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"), batchImport);
+ var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"), batchImport);
assertImportedOnce(import1, import2);
});
@@ -134,10 +134,10 @@ namespace osu.Game.Tests.Skins.IO
});
[Test]
- public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu =>
+ public Task TestSameMetadataNameSameFolderName([Values] bool batchImport) => runSkinTest(async osu =>
{
- var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
- var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
+ var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"), batchImport);
+ var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"), batchImport);
assertImportedOnce(import1, import2);
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
@@ -357,10 +357,10 @@ namespace osu.Game.Tests.Skins.IO
}
}
- private async Task> loadSkinIntoOsu(OsuGameBase osu, ImportTask import)
+ private async Task> loadSkinIntoOsu(OsuGameBase osu, ImportTask import, bool batchImport = false)
{
var skinManager = osu.Dependencies.Get();
- return await skinManager.Import(import);
+ return await skinManager.Import(import, batchImport);
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
index 6b5d9af7af..291630fa3a 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using NUnit.Framework;
@@ -24,7 +22,7 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene
{
- private EditorBeatmap editorBeatmap;
+ private EditorBeatmap editorBeatmap = null!;
[Cached]
private EditorClipboard clipboard = new EditorClipboard();
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index 2707682b4c..f565ca3ef4 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.IO;
using System.Linq;
@@ -39,7 +37,9 @@ namespace osu.Game.Tests.Visual.Editing
protected override bool IsolateSavingFromDatabase => false;
[Resolved]
- private BeatmapManager beatmapManager { get; set; }
+ private BeatmapManager beatmapManager { get; set; } = null!;
+
+ private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
public override void SetUpSteps()
{
@@ -50,19 +50,19 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString());
}
- protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new DummyWorkingBeatmap(Audio, null);
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new DummyWorkingBeatmap(Audio, null);
[Test]
public void TestCreateNewBeatmap()
{
AddStep("save beatmap", () => Editor.Save());
- AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == false);
+ AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID)?.Value.DeletePending == false);
}
[Test]
public void TestExitWithoutSave()
{
- EditorBeatmap editorBeatmap = null;
+ EditorBeatmap editorBeatmap = null!;
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
- AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true);
+ AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull().ID)?.Value.DeletePending == true);
}
[Test]
@@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("new beatmap persisted", () =>
{
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName);
- var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
+ var set = beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID);
return beatmap != null
&& beatmap.DifficultyName == firstDifficultyName
@@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
- string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
+ string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != firstDifficultyName;
});
@@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("new beatmap persisted", () =>
{
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName);
- var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
+ var set = beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID);
return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName
@@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("new beatmap persisted", () =>
{
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == originalDifficultyName);
- var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
+ var set = beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID);
return beatmap != null
&& beatmap.DifficultyName == originalDifficultyName
@@ -262,7 +262,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
- string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
+ string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != originalDifficultyName;
});
@@ -281,13 +281,13 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("save beatmap", () => Editor.Save());
- BeatmapInfo refetchedBeatmap = null;
- Live refetchedBeatmapSet = null;
+ BeatmapInfo? refetchedBeatmap = null;
+ Live? refetchedBeatmapSet = null;
AddStep("refetch from database", () =>
{
refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == copyDifficultyName);
- refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
+ refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID);
});
AddAssert("new beatmap persisted", () =>
@@ -323,7 +323,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
- string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
+ string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != "New Difficulty";
});
AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)");
@@ -359,7 +359,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
- string difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
+ string? difficultyName = Editor.ChildrenOfType().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != duplicate_difficulty_name;
});
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index bcf02cd814..8e325838ff 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Tests.Visual.Editing
SaveEditor();
+ AddAssert("Hash updated", () => !string.IsNullOrEmpty(EditorBeatmap.BeatmapInfo.BeatmapSet?.Hash));
+
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");
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
index 14b2593fa7..720e32a242 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs
@@ -24,10 +24,11 @@ namespace osu.Game.Tests.Visual.Menus
public void TestMusicPlayAction()
{
AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething());
+ AddUntilStep("music playing", () => Game.MusicController.IsPlaying);
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
- AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.UserPauseRequested);
+ AddUntilStep("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.UserPauseRequested);
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
- AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.UserPauseRequested);
+ AddUntilStep("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.UserPauseRequested);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index 877c986d61..7df68392cf 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -33,20 +31,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
{
[Resolved]
- private OsuGameBase game { get; set; }
+ private OsuGameBase game { get; set; } = null!;
[Resolved]
- private OsuConfigManager config { get; set; }
+ private OsuConfigManager config { get; set; } = null!;
[Resolved]
- private BeatmapManager beatmapManager { get; set; }
+ private BeatmapManager beatmapManager { get; set; } = null!;
- private MultiSpectatorScreen spectatorScreen;
+ private MultiSpectatorScreen spectatorScreen = null!;
private readonly List playingUsers = new List();
- private BeatmapSetInfo importedSet;
- private BeatmapInfo importedBeatmap;
+ private BeatmapSetInfo importedSet = null!;
+ private BeatmapInfo importedBeatmap = null!;
+
private int importedBeatmapId;
[BackgroundDependencyLoader]
@@ -340,7 +339,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
sendFrames(getPlayerIds(count), 300);
}
- Player player = null;
+ Player? player = null;
AddStep($"get {PLAYER_1_ID} player instance", () => player = getInstance(PLAYER_1_ID).ChildrenOfType().Single());
@@ -369,7 +368,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
b.Storyboard.GetLayer("Background").Add(sprite);
});
- private void testLeadIn(Action applyToBeatmap = null)
+ private void testLeadIn(Action? applyToBeatmap = null)
{
start(PLAYER_1_ID);
@@ -387,7 +386,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertRunning(PLAYER_1_ID);
}
- private void loadSpectateScreen(bool waitForPlayerLoad = true, Action applyToBeatmap = null)
+ private void loadSpectateScreen(bool waitForPlayerLoad = true, Action? applyToBeatmap = null)
{
AddStep("load screen", () =>
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index da48fb7332..a2793acba7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Diagnostics;
using System.Linq;
@@ -51,17 +49,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayer : ScreenTestScene
{
- private BeatmapManager beatmaps;
- private RulesetStore rulesets;
- private BeatmapSetInfo importedSet;
+ private BeatmapManager beatmaps = null!;
+ private RulesetStore rulesets = null!;
+ private BeatmapSetInfo importedSet = null!;
- private TestMultiplayerComponents multiplayerComponents;
+ private TestMultiplayerComponents multiplayerComponents = null!;
private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient;
private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
[Resolved]
- private OsuConfigManager config { get; set; }
+ private OsuConfigManager config { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -146,7 +144,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void removeLastUser()
{
- APIUser lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
+ APIUser? lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
return;
@@ -156,7 +154,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void kickLastUser()
{
- APIUser lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
+ APIUser? lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
return;
@@ -351,7 +349,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
- DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
+ DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick());
@@ -678,7 +676,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestGameplayExitFlow()
{
- Bindable holdDelay = null;
+ Bindable? holdDelay = null;
AddStep("Set hold delay to zero", () =>
{
@@ -709,7 +707,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for lounge", () => multiplayerComponents.CurrentScreen is Screens.OnlinePlay.Multiplayer.Multiplayer);
AddStep("stop holding", () => InputManager.ReleaseKey(Key.Escape));
- AddStep("set hold delay to default", () => holdDelay.SetDefault());
+ AddStep("set hold delay to default", () => holdDelay?.SetDefault());
}
[Test]
@@ -992,7 +990,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value);
MultiplayerUserState lastState = MultiplayerUserState.Idle;
- MultiplayerRoomUser user = null;
+ MultiplayerRoomUser? user = null;
AddStep("click ready button", () =>
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index 7db18d1127..a70dfd78c5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -66,7 +64,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRemoveUser()
{
- APIUser secondUser = null;
+ APIUser? secondUser = null;
AddStep("add a user", () =>
{
@@ -80,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value));
- AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.UserID == secondUser.Id);
+ AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.UserID == secondUser?.Id);
}
[Test]
@@ -368,7 +366,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewParticipantsList()
{
- ParticipantsList participantsList = null;
+ ParticipantsList? participantsList = null;
AddStep("create new list", () => Child = participantsList = new ParticipantsList
{
@@ -378,7 +376,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Size = new Vector2(380, 0.7f)
});
- AddUntilStep("wait for list to load", () => participantsList.IsLoaded);
+ AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);
}
private void checkProgressBarVisibility(bool visible) =>
diff --git a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs
index 8ab8276b9c..10d9a5664e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Overlays.BeatmapSet;
using System.Collections.Specialized;
using System.Linq;
@@ -29,7 +27,7 @@ namespace osu.Game.Tests.Visual.Online
LeaderboardModSelector modSelector;
FillFlowContainer selectedMods;
- var ruleset = new Bindable();
+ var ruleset = new Bindable();
Add(selectedMods = new FillFlowContainer
{
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index 5382b98c22..92f1fc17d5 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -31,12 +31,11 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
- private readonly BeatmapUpdater? beatmapUpdater;
+ public Action? ProcessBeatmap { private get; set; }
- public BeatmapImporter(Storage storage, RealmAccess realm, BeatmapUpdater? beatmapUpdater = null)
+ public BeatmapImporter(Storage storage, RealmAccess realm)
: base(storage, realm)
{
- this.beatmapUpdater = beatmapUpdater;
}
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
@@ -100,7 +99,7 @@ namespace osu.Game.Beatmaps
{
base.PostImport(model, realm);
- beatmapUpdater?.Process(model);
+ ProcessBeatmap?.Invoke(model);
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index e16a87eb50..30456afd2f 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -34,14 +34,15 @@ namespace osu.Game.Beatmaps
/// Handles general operations related to global beatmap management.
///
[ExcludeFromDynamicCompile]
- public class BeatmapManager : ModelManager, IModelImporter, IWorkingBeatmapCache, IDisposable
+ public class BeatmapManager : ModelManager, IModelImporter, IWorkingBeatmapCache
{
public ITrackStore BeatmapTrackStore { get; }
private readonly BeatmapImporter beatmapImporter;
private readonly WorkingBeatmapCache workingBeatmapCache;
- private readonly BeatmapUpdater? beatmapUpdater;
+
+ public Action? ProcessBeatmap { private get; set; }
public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore gameResources, GameHost? host = null,
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
@@ -54,15 +55,14 @@ namespace osu.Game.Beatmaps
if (difficultyCache == null)
throw new ArgumentNullException(nameof(difficultyCache), "Difficulty cache must be provided if online lookups are required.");
-
- beatmapUpdater = new BeatmapUpdater(this, difficultyCache, api, storage);
}
var userResources = new RealmFileStore(realm, storage).Store;
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
- beatmapImporter = CreateBeatmapImporter(storage, realm, rulesets, beatmapUpdater);
+ beatmapImporter = CreateBeatmapImporter(storage, realm);
+ beatmapImporter.ProcessBeatmap = obj => ProcessBeatmap?.Invoke(obj);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
@@ -74,8 +74,7 @@ namespace osu.Game.Beatmaps
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
}
- protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapUpdater? beatmapUpdater) =>
- new BeatmapImporter(storage, realm, beatmapUpdater);
+ protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm);
///
/// Create a new beatmap set, backed by a model,
@@ -317,13 +316,15 @@ namespace osu.Game.Beatmaps
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
+ setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
+
Realm.Write(r =>
{
var liveBeatmapSet = r.Find(setInfo.ID);
setInfo.CopyChangesToRealm(liveBeatmapSet);
- beatmapUpdater?.Process(liveBeatmapSet, r);
+ ProcessBeatmap?.Invoke(liveBeatmapSet);
});
}
@@ -468,15 +469,6 @@ namespace osu.Game.Beatmaps
#endregion
- #region Implementation of IDisposable
-
- public void Dispose()
- {
- beatmapUpdater?.Dispose();
- }
-
- #endregion
-
#region Implementation of IPostImports
public Action>>? PresentImport
diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs
index d800b09a2b..20fa0bc7c6 100644
--- a/osu.Game/Beatmaps/BeatmapUpdater.cs
+++ b/osu.Game/Beatmaps/BeatmapUpdater.cs
@@ -10,7 +10,6 @@ using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Rulesets.Objects;
-using Realms;
namespace osu.Game.Beatmaps
{
@@ -31,6 +30,14 @@ namespace osu.Game.Beatmaps
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
}
+ ///
+ /// Queue a beatmap for background processing.
+ ///
+ public void Queue(int beatmapSetId)
+ {
+ // TODO: implement
+ }
+
///
/// Queue a beatmap for background processing.
///
@@ -44,9 +51,7 @@ namespace osu.Game.Beatmaps
///
/// Run all processing on a beatmap immediately.
///
- public void Process(BeatmapSetInfo beatmapSet) => beatmapSet.Realm.Write(r => Process(beatmapSet, r));
-
- public void Process(BeatmapSetInfo beatmapSet, Realm realm)
+ public void Process(BeatmapSetInfo beatmapSet) => beatmapSet.Realm.Write(r =>
{
// Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet);
@@ -71,7 +76,7 @@ namespace osu.Game.Beatmaps
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
workingBeatmapCache.Invalidate(beatmapSet);
- }
+ });
private double calculateLength(IBeatmap b)
{
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index 9d31c58709..ce883a7092 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -137,8 +137,17 @@ namespace osu.Game.Beatmaps
try
{
- using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
- return Decoder.GetDecoder(stream).Decode(stream);
+ string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
+ var stream = GetStream(fileStorePath);
+
+ if (stream == null)
+ {
+ Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
+ return null;
+ }
+
+ using (var reader = new LineBufferedReader(stream))
+ return Decoder.GetDecoder(reader).Decode(reader);
}
catch (Exception e)
{
@@ -154,7 +163,16 @@ namespace osu.Game.Beatmaps
try
{
- return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile));
+ string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile);
+ var texture = resources.LargeTextureStore.Get(fileStorePath);
+
+ if (texture == null)
+ {
+ Logger.Log($"Beatmap background failed to load (file {Metadata.BackgroundFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
+ return null;
+ }
+
+ return texture;
}
catch (Exception e)
{
@@ -173,7 +191,16 @@ namespace osu.Game.Beatmaps
try
{
- return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
+ string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.AudioFile);
+ var track = resources.Tracks.Get(fileStorePath);
+
+ if (track == null)
+ {
+ Logger.Log($"Beatmap failed to load (file {Metadata.AudioFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
+ return null;
+ }
+
+ return track;
}
catch (Exception e)
{
@@ -192,8 +219,17 @@ namespace osu.Game.Beatmaps
try
{
- var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
- return trackData == null ? null : new Waveform(trackData);
+ string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.AudioFile);
+
+ var trackData = GetStream(fileStorePath);
+
+ if (trackData == null)
+ {
+ Logger.Log($"Beatmap waveform failed to load (file {Metadata.AudioFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
+ return null;
+ }
+
+ return new Waveform(trackData);
}
catch (Exception e)
{
@@ -211,20 +247,38 @@ namespace osu.Game.Beatmaps
try
{
- using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
+ string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
+ var beatmapFileStream = GetStream(fileStorePath);
+
+ if (beatmapFileStream == null)
{
- var decoder = Decoder.GetDecoder(stream);
+ Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error);
+ return null;
+ }
- string storyboardFilename = BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
+ using (var reader = new LineBufferedReader(beatmapFileStream))
+ {
+ var decoder = Decoder.GetDecoder(reader);
- // todo: support loading from both set-wide storyboard *and* beatmap specific.
- if (string.IsNullOrEmpty(storyboardFilename))
- storyboard = decoder.Decode(stream);
- else
+ Stream storyboardFileStream = null;
+
+ if (BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename is string storyboardFilename)
{
- using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename))))
- storyboard = decoder.Decode(stream, secondaryStream);
+ string storyboardFileStorePath = BeatmapSetInfo?.GetPathForFile(storyboardFilename);
+ storyboardFileStream = GetStream(storyboardFileStorePath);
+
+ if (storyboardFileStream == null)
+ Logger.Log($"Storyboard failed to load (file {storyboardFilename} not found on disk at expected location {storyboardFileStorePath})", level: LogLevel.Error);
}
+
+ if (storyboardFileStream != null)
+ {
+ // Stand-alone storyboard was found, so parse in addition to the beatmap's local storyboard.
+ using (var secondaryReader = new LineBufferedReader(storyboardFileStream))
+ storyboard = decoder.Decode(reader, secondaryReader);
+ }
+ else
+ storyboard = decoder.Decode(reader);
}
}
catch (Exception e)
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 713166a9a0..a523507205 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -167,6 +167,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
+
+ SetDefault(OsuSetting.LastProcessedMetadataId, -1);
}
public IDictionary GetLoggableState() =>
@@ -363,5 +365,6 @@ namespace osu.Game.Configuration
DiscordRichPresence,
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent,
+ LastProcessedMetadataId
}
}
diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs
index 76f6db1384..6cea92f1d9 100644
--- a/osu.Game/Database/RealmArchiveModelImporter.cs
+++ b/osu.Game/Database/RealmArchiveModelImporter.cs
@@ -258,15 +258,13 @@ namespace osu.Game.Database
{
cancellationToken.ThrowIfCancellationRequested();
- bool checkedExisting = false;
- TModel? existing = null;
+ TModel? existing;
if (batchImport && archive != null)
{
// this is a fast bail condition to improve large import performance.
item.Hash = computeHashFast(archive);
- checkedExisting = true;
existing = CheckForExisting(item, realm);
if (existing != null)
@@ -311,8 +309,12 @@ namespace osu.Game.Database
// TODO: we may want to run this outside of the transaction.
Populate(item, archive, realm, cancellationToken);
- if (!checkedExisting)
- existing = CheckForExisting(item, realm);
+ // Populate() may have adjusted file content (see SkinImporter.updateSkinIniMetadata), so regardless of whether a fast check was done earlier, let's
+ // check for existing items a second time.
+ //
+ // If this is ever a performance issue, the fast-check hash can be compared and trigger a skip of this second check if it still matches.
+ // I don't think it is a huge deal doing a second indexed check, though.
+ existing = CheckForExisting(item, realm);
if (existing != null)
{
@@ -386,7 +388,7 @@ namespace osu.Game.Database
///
/// In the case of no matching files, a hash will be generated from the passed archive's .
///
- protected string ComputeHash(TModel item)
+ public string ComputeHash(TModel item)
{
// for now, concatenate all hashable files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs
index 73e9f16d33..13c4defb83 100644
--- a/osu.Game/Database/RealmExtensions.cs
+++ b/osu.Game/Database/RealmExtensions.cs
@@ -8,19 +8,60 @@ namespace osu.Game.Database
{
public static class RealmExtensions
{
+ ///
+ /// Perform a write operation against the provided realm instance.
+ ///
+ ///
+ /// This will automatically start a transaction if not already in one.
+ ///
+ /// The realm to operate on.
+ /// The write operation to run.
public static void Write(this Realm realm, Action function)
{
- using var transaction = realm.BeginWrite();
- function(realm);
- transaction.Commit();
+ Transaction? transaction = null;
+
+ try
+ {
+ if (!realm.IsInTransaction)
+ transaction = realm.BeginWrite();
+
+ function(realm);
+
+ transaction?.Commit();
+ }
+ finally
+ {
+ transaction?.Dispose();
+ }
}
+ ///
+ /// Perform a write operation against the provided realm instance.
+ ///
+ ///
+ /// This will automatically start a transaction if not already in one.
+ ///
+ /// The realm to operate on.
+ /// The write operation to run.
public static T Write(this Realm realm, Func function)
{
- using var transaction = realm.BeginWrite();
- var result = function(realm);
- transaction.Commit();
- return result;
+ Transaction? transaction = null;
+
+ try
+ {
+ if (!realm.IsInTransaction)
+ transaction = realm.BeginWrite();
+
+ var result = function(realm);
+
+ transaction?.Commit();
+
+ return result;
+ }
+ finally
+ {
+ transaction?.Dispose();
+ }
}
///
diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs
index f2caf10e91..99af95b5fe 100644
--- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs
+++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs
@@ -38,15 +38,21 @@ namespace osu.Game.Graphics.Backgrounds
private void load(OsuConfigManager config, SessionStatics sessionStatics)
{
seasonalBackgroundMode = config.GetBindable(OsuSetting.SeasonalBackgroundMode);
- seasonalBackgroundMode.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
+ seasonalBackgroundMode.BindValueChanged(_ => triggerSeasonalBackgroundChanged());
seasonalBackgrounds = sessionStatics.GetBindable(Static.SeasonalBackgrounds);
- seasonalBackgrounds.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
+ seasonalBackgrounds.BindValueChanged(_ => triggerSeasonalBackgroundChanged());
apiState.BindTo(api.State);
apiState.BindValueChanged(fetchSeasonalBackgrounds, true);
}
+ private void triggerSeasonalBackgroundChanged()
+ {
+ if (shouldShowSeasonal)
+ SeasonalBackgroundChanged?.Invoke();
+ }
+
private void fetchSeasonalBackgrounds(ValueChangedEvent stateChanged)
{
if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online)
@@ -64,15 +70,10 @@ namespace osu.Game.Graphics.Backgrounds
public SeasonalBackground LoadNextBackground()
{
- if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Never
- || (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && !isInSeason))
- {
+ if (!shouldShowSeasonal)
return null;
- }
- var backgrounds = seasonalBackgrounds.Value?.Backgrounds;
- if (backgrounds == null || !backgrounds.Any())
- return null;
+ var backgrounds = seasonalBackgrounds.Value.Backgrounds;
current = (current + 1) % backgrounds.Count;
string url = backgrounds[current].Url;
@@ -80,6 +81,20 @@ namespace osu.Game.Graphics.Backgrounds
return new SeasonalBackground(url);
}
+ private bool shouldShowSeasonal
+ {
+ get
+ {
+ if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Never)
+ return false;
+
+ if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && !isInSeason)
+ return false;
+
+ return seasonalBackgrounds.Value?.Backgrounds?.Any() == true;
+ }
+ }
+
private bool isInSeason => seasonalBackgrounds.Value != null && DateTimeOffset.Now < seasonalBackgrounds.Value.EndDate;
}
diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs
index 83fd02512b..3171d15fc2 100644
--- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs
+++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Online
APIClientID = "5";
SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator";
MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer";
+ MetadataEndpointUrl = $"{APIEndpointUrl}/metadata";
}
}
}
diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs
index af4e88a05c..f3bcced630 100644
--- a/osu.Game/Online/EndpointConfiguration.cs
+++ b/osu.Game/Online/EndpointConfiguration.cs
@@ -39,5 +39,10 @@ namespace osu.Game.Online
/// The endpoint for the SignalR multiplayer server.
///
public string MultiplayerEndpointUrl { get; set; }
+
+ ///
+ /// The endpoint for the SignalR metadata server.
+ ///
+ public string MetadataEndpointUrl { get; set; }
}
}
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index 61e9eaa8c0..01f0f3a902 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -144,7 +144,7 @@ namespace osu.Game.Online
///
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
{
- Logger.Log($"{clientName} connection error: {exception}", LoggingTarget.Network);
+ Logger.Log($"{clientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}
diff --git a/osu.Game/Online/Metadata/BeatmapUpdates.cs b/osu.Game/Online/Metadata/BeatmapUpdates.cs
new file mode 100644
index 0000000000..a0cf616c70
--- /dev/null
+++ b/osu.Game/Online/Metadata/BeatmapUpdates.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using MessagePack;
+
+namespace osu.Game.Online.Metadata
+{
+ ///
+ /// Describes a set of beatmaps which have been updated in some way.
+ ///
+ [MessagePackObject]
+ [Serializable]
+ public class BeatmapUpdates
+ {
+ [Key(0)]
+ public int[] BeatmapSetIDs { get; set; }
+
+ [Key(1)]
+ public int LastProcessedQueueID { get; set; }
+
+ public BeatmapUpdates(int[] beatmapSetIDs, int lastProcessedQueueID)
+ {
+ BeatmapSetIDs = beatmapSetIDs;
+ LastProcessedQueueID = lastProcessedQueueID;
+ }
+ }
+}
diff --git a/osu.Game/Online/Metadata/IMetadataClient.cs b/osu.Game/Online/Metadata/IMetadataClient.cs
new file mode 100644
index 0000000000..ad1e7ebbaf
--- /dev/null
+++ b/osu.Game/Online/Metadata/IMetadataClient.cs
@@ -0,0 +1,12 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.Tasks;
+
+namespace osu.Game.Online.Metadata
+{
+ public interface IMetadataClient
+ {
+ Task BeatmapSetsUpdated(BeatmapUpdates updates);
+ }
+}
diff --git a/osu.Game/Online/Metadata/IMetadataServer.cs b/osu.Game/Online/Metadata/IMetadataServer.cs
new file mode 100644
index 0000000000..994f60f877
--- /dev/null
+++ b/osu.Game/Online/Metadata/IMetadataServer.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.Tasks;
+
+namespace osu.Game.Online.Metadata
+{
+ ///
+ /// Metadata server is responsible for keeping the osu! client up-to-date with any changes.
+ ///
+ public interface IMetadataServer
+ {
+ ///
+ /// Get any changes since a specific point in the queue.
+ /// Should be used to allow the client to catch up with any changes after being closed or disconnected.
+ ///
+ /// The last processed queue ID.
+ ///
+ Task GetChangesSince(int queueId);
+ }
+}
diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs
new file mode 100644
index 0000000000..1e5eeb4eb0
--- /dev/null
+++ b/osu.Game/Online/Metadata/MetadataClient.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.Tasks;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Online.Metadata
+{
+ public abstract class MetadataClient : Component, IMetadataClient, IMetadataServer
+ {
+ public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates);
+
+ public abstract Task GetChangesSince(int queueId);
+ }
+}
diff --git a/osu.Game/Online/Metadata/OnlineMetadataClient.cs b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
new file mode 100644
index 0000000000..1b0d1884dc
--- /dev/null
+++ b/osu.Game/Online/Metadata/OnlineMetadataClient.cs
@@ -0,0 +1,134 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR.Client;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Logging;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Online.API;
+
+namespace osu.Game.Online.Metadata
+{
+ public class OnlineMetadataClient : MetadataClient
+ {
+ private readonly BeatmapUpdater beatmapUpdater;
+ private readonly string endpoint;
+
+ private IHubClientConnector? connector;
+
+ private Bindable lastQueueId = null!;
+
+ private HubConnection? connection => connector?.CurrentConnection;
+
+ public OnlineMetadataClient(EndpointConfiguration endpoints, BeatmapUpdater beatmapUpdater)
+ {
+ this.beatmapUpdater = beatmapUpdater;
+ endpoint = endpoints.MetadataEndpointUrl;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IAPIProvider api, OsuConfigManager config)
+ {
+ // Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization.
+ // More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code.
+ connector = api.GetHubConnector(nameof(OnlineMetadataClient), endpoint);
+
+ if (connector != null)
+ {
+ connector.ConfigureConnection = connection =>
+ {
+ // this is kind of SILLY
+ // https://github.com/dotnet/aspnetcore/issues/15198
+ connection.On(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
+ };
+
+ connector.IsConnected.BindValueChanged(isConnectedChanged, true);
+ }
+
+ lastQueueId = config.GetBindable(OsuSetting.LastProcessedMetadataId);
+ }
+
+ private bool catchingUp;
+
+ private void isConnectedChanged(ValueChangedEvent connected)
+ {
+ if (!connected.NewValue)
+ return;
+
+ if (lastQueueId.Value >= 0)
+ {
+ catchingUp = true;
+
+ Task.Run(async () =>
+ {
+ try
+ {
+ while (true)
+ {
+ Logger.Log($"Requesting catch-up from {lastQueueId.Value}");
+ var catchUpChanges = await GetChangesSince(lastQueueId.Value);
+
+ lastQueueId.Value = catchUpChanges.LastProcessedQueueID;
+
+ if (catchUpChanges.BeatmapSetIDs.Length == 0)
+ {
+ Logger.Log($"Catch-up complete at {lastQueueId.Value}");
+ break;
+ }
+
+ await ProcessChanges(catchUpChanges.BeatmapSetIDs);
+ }
+ }
+ finally
+ {
+ catchingUp = false;
+ }
+ });
+ }
+ }
+
+ public override async Task BeatmapSetsUpdated(BeatmapUpdates updates)
+ {
+ Logger.Log($"Received beatmap updates {updates.BeatmapSetIDs.Length} updates with last id {updates.LastProcessedQueueID}");
+
+ // If we're still catching up, avoid updating the last ID as it will interfere with catch-up efforts.
+ if (!catchingUp)
+ lastQueueId.Value = updates.LastProcessedQueueID;
+
+ await ProcessChanges(updates.BeatmapSetIDs);
+ }
+
+ protected Task ProcessChanges(int[] beatmapSetIDs)
+ {
+ foreach (int id in beatmapSetIDs)
+ {
+ Logger.Log($"Processing {id}...");
+ beatmapUpdater.Queue(id);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public override Task GetChangesSince(int queueId)
+ {
+ if (connector?.IsConnected.Value != true)
+ return Task.FromCanceled(default);
+
+ Logger.Log($"Requesting any changes since last known queue id {queueId}");
+
+ Debug.Assert(connection != null);
+
+ return connection.InvokeAsync(nameof(IMetadataServer.GetChangesSince), queueId);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ connector?.Dispose();
+ }
+ }
+}
diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs
index f431beac1c..316452280d 100644
--- a/osu.Game/Online/ProductionEndpointConfiguration.cs
+++ b/osu.Game/Online/ProductionEndpointConfiguration.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Online
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
+ MetadataEndpointUrl = "https://spectator.ppy.sh/metadata";
}
}
}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index c6a32e1d5b..a16252ac89 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
@@ -41,6 +40,7 @@ using osu.Game.IO;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
+using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
@@ -52,6 +52,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Utils;
+using File = System.IO.File;
using RuntimeInfo = osu.Framework.RuntimeInfo;
namespace osu.Game
@@ -170,6 +171,7 @@ namespace osu.Game
public readonly Bindable>> AvailableMods = new Bindable>>(new Dictionary>());
private BeatmapDifficultyCache difficultyCache;
+ private BeatmapUpdater beatmapUpdater;
private UserLookupCache userCache;
private BeatmapLookupCache beatmapCache;
@@ -180,6 +182,8 @@ namespace osu.Game
private MultiplayerClient multiplayerClient;
+ private MetadataClient metadataClient;
+
private RealmAccess realm;
protected override Container Content => content;
@@ -263,15 +267,13 @@ namespace osu.Game
dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash));
- dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
- dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
-
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig));
+
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
@@ -280,6 +282,15 @@ namespace osu.Game
// Add after all the above cache operations as it depends on them.
AddInternal(difficultyCache);
+ // TODO: OsuGame or OsuGameBase?
+ beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage);
+
+ dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
+ dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
+ dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints, beatmapUpdater));
+
+ BeatmapManager.ProcessBeatmap = set => beatmapUpdater.Process(set);
+
dependencies.Cache(userCache = new UserLookupCache());
AddInternal(userCache);
@@ -316,8 +327,10 @@ namespace osu.Game
// add api components to hierarchy.
if (API is APIAccess apiAccess)
AddInternal(apiAccess);
+
AddInternal(spectatorClient);
AddInternal(multiplayerClient);
+ AddInternal(metadataClient);
AddInternal(rulesetConfigCache);
@@ -574,9 +587,10 @@ namespace osu.Game
base.Dispose(isDisposing);
RulesetStore?.Dispose();
- BeatmapManager?.Dispose();
LocalConfig?.Dispose();
+ beatmapUpdater?.Dispose();
+
realm?.Dispose();
if (Host != null)
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 4a10f30a7a..8af295dfe8 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -133,9 +133,9 @@ namespace osu.Game.Overlays
UserPauseRequested = false;
if (restart)
- CurrentTrack.Restart();
+ CurrentTrack.RestartAsync();
else if (!IsPlaying)
- CurrentTrack.Start();
+ CurrentTrack.StartAsync();
return true;
}
@@ -152,7 +152,7 @@ namespace osu.Game.Overlays
{
UserPauseRequested |= requestedByUser;
if (CurrentTrack.IsRunning)
- CurrentTrack.Stop();
+ CurrentTrack.StopAsync();
}
///
@@ -250,7 +250,7 @@ namespace osu.Game.Overlays
{
// if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).
// we probably want to move this to a central method for switching to a new working beatmap in the future.
- Schedule(() => CurrentTrack.Restart());
+ Schedule(() => CurrentTrack.RestartAsync());
}
private WorkingBeatmap current;
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 741b6b5815..d23ef7e3e7 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -62,7 +62,8 @@ namespace osu.Game.Overlays.Settings.Sections
{
skinDropdown = new SkinSettingsDropdown
{
- LabelText = SkinSettingsStrings.CurrentSkin
+ LabelText = SkinSettingsStrings.CurrentSkin,
+ Keywords = new[] { @"skins" }
},
new SettingsButton
{
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
index 95bcb2ab29..c794c768c6 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -7,6 +7,7 @@ using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -26,7 +27,7 @@ namespace osu.Game.Screens.Backgrounds
private const int background_count = 7;
private IBindable user;
private Bindable skin;
- private Bindable mode;
+ private Bindable source;
private Bindable introSequence;
private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader();
@@ -45,24 +46,29 @@ namespace osu.Game.Screens.Backgrounds
{
user = api.LocalUser.GetBoundCopy();
skin = skinManager.CurrentSkin.GetBoundCopy();
- mode = config.GetBindable(OsuSetting.MenuBackgroundSource);
+ source = config.GetBindable(OsuSetting.MenuBackgroundSource);
introSequence = config.GetBindable(OsuSetting.IntroSequence);
AddInternal(seasonalBackgroundLoader);
- user.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
- skin.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
- mode.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
- beatmap.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
- introSequence.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
- seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Scheduler.AddOnce(loadNextIfRequired);
-
+ // Load first background asynchronously as part of BDL load.
currentDisplay = RNG.Next(0, background_count);
-
Next();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ user.ValueChanged += _ => Scheduler.AddOnce(next);
+ skin.ValueChanged += _ => Scheduler.AddOnce(next);
+ source.ValueChanged += _ => Scheduler.AddOnce(next);
+ beatmap.ValueChanged += _ => Scheduler.AddOnce(next);
+ introSequence.ValueChanged += _ => Scheduler.AddOnce(next);
+ seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Scheduler.AddOnce(next);
// helper function required for AddOnce usage.
- void loadNextIfRequired() => Next();
+ void next() => Next();
}
private ScheduledDelegate nextTask;
@@ -80,6 +86,8 @@ namespace osu.Game.Screens.Backgrounds
if (nextBackground == background)
return false;
+ Logger.Log("🌅 Background change queued");
+
cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
@@ -108,12 +116,12 @@ namespace osu.Game.Screens.Backgrounds
if (newBackground == null && user.Value?.IsSupporter == true)
{
- switch (mode.Value)
+ switch (source.Value)
{
case BackgroundSource.Beatmap:
case BackgroundSource.BeatmapWithStoryboard:
{
- if (mode.Value == BackgroundSource.BeatmapWithStoryboard && AllowStoryboardBackground)
+ if (source.Value == BackgroundSource.BeatmapWithStoryboard && AllowStoryboardBackground)
newBackground = new BeatmapBackgroundWithStoryboard(beatmap.Value, getBackgroundTextureName());
newBackground ??= new BeatmapBackground(beatmap.Value, getBackgroundTextureName());
diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs
index c81195bbd3..c1621ce78f 100644
--- a/osu.Game/Screens/Menu/IntroScreen.cs
+++ b/osu.Game/Screens/Menu/IntroScreen.cs
@@ -88,6 +88,11 @@ namespace osu.Game.Screens.Menu
///
protected bool UsingThemedIntro { get; private set; }
+ protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(false)
+ {
+ Colour = Color4.Black
+ };
+
protected IntroScreen([CanBeNull] Func createNextScreen = null)
{
this.createNextScreen = createNextScreen;
@@ -201,6 +206,8 @@ namespace osu.Game.Screens.Menu
{
this.FadeIn(300);
+ ApplyToBackground(b => b.FadeColour(Color4.Black, 100));
+
double fadeOutTime = exit_delay;
var track = musicController.CurrentTrack;
@@ -243,13 +250,22 @@ namespace osu.Game.Screens.Menu
base.OnResuming(e);
}
+ private bool backgroundFaded;
+
+ protected void FadeInBackground(float duration = 0)
+ {
+ ApplyToBackground(b => b.FadeColour(Color4.White, duration));
+ backgroundFaded = true;
+ }
+
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
initialBeatmap = null;
- }
- protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack();
+ if (!backgroundFaded)
+ FadeInBackground(200);
+ }
protected virtual void StartTrack()
{
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index ad098ae8df..3cdf51a87c 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -8,19 +8,18 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
-using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
-using osu.Framework.Utils;
+using osu.Framework.Screens;
using osu.Framework.Timing;
+using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
-using osu.Game.Screens.Backgrounds;
using osuTK;
using osuTK.Graphics;
@@ -32,16 +31,9 @@ namespace osu.Game.Screens.Menu
protected override string BeatmapFile => "triangles.osz";
- protected override BackgroundScreen CreateBackground() => background = new BackgroundScreenDefault(false)
- {
- Alpha = 0,
- };
-
[Resolved]
private AudioManager audio { get; set; }
- private BackgroundScreenDefault background;
-
private Sample welcome;
private DecoupleableInterpolatingFramedClock decoupledClock;
@@ -75,7 +67,7 @@ namespace osu.Game.Screens.Menu
if (UsingThemedIntro)
decoupledClock.ChangeSource(Track);
- LoadComponentAsync(intro = new TrianglesIntroSequence(logo, background)
+ LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground())
{
RelativeSizeAxes = Axes.Both,
Clock = decoupledClock,
@@ -95,19 +87,10 @@ namespace osu.Game.Screens.Menu
{
base.OnSuspending(e);
- // ensure the background is shown, even if the TriangleIntroSequence failed to do so.
- background.ApplyToBackground(b => b.Show());
-
// important as there is a clock attached to a track which will likely be disposed before returning to this screen.
intro.Expire();
}
- public override void OnResuming(ScreenTransitionEvent e)
- {
- base.OnResuming(e);
- background.FadeOut(100);
- }
-
protected override void StartTrack()
{
decoupledClock.Start();
@@ -116,7 +99,7 @@ namespace osu.Game.Screens.Menu
private class TrianglesIntroSequence : CompositeDrawable
{
private readonly OsuLogo logo;
- private readonly BackgroundScreenDefault background;
+ private readonly Action showBackgroundAction;
private OsuSpriteText welcomeText;
private RulesetFlow rulesets;
@@ -128,10 +111,10 @@ namespace osu.Game.Screens.Menu
public Action LoadMenu;
- public TrianglesIntroSequence(OsuLogo logo, BackgroundScreenDefault background)
+ public TrianglesIntroSequence(OsuLogo logo, Action showBackgroundAction)
{
this.logo = logo;
- this.background = background;
+ this.showBackgroundAction = showBackgroundAction;
}
[Resolved]
@@ -205,7 +188,6 @@ namespace osu.Game.Screens.Menu
rulesets.Hide();
lazerLogo.Hide();
- background.ApplyToBackground(b => b.Hide());
using (BeginAbsoluteSequence(0))
{
@@ -267,7 +249,7 @@ namespace osu.Game.Screens.Menu
logo.FadeIn();
- background.ApplyToBackground(b => b.Show());
+ showBackgroundAction();
game.Add(new GameWideFlash());
diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs
index 031c8d7902..9e56a3a0b7 100644
--- a/osu.Game/Screens/Menu/IntroWelcome.cs
+++ b/osu.Game/Screens/Menu/IntroWelcome.cs
@@ -5,11 +5,9 @@
using System;
using JetBrains.Annotations;
-using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
-using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -17,8 +15,8 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Online.API;
-using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
@@ -35,13 +33,6 @@ namespace osu.Game.Screens.Menu
private ISample pianoReverb;
protected override string SeeyaSampleName => "Intro/Welcome/seeya";
- protected override BackgroundScreen CreateBackground() => background = new BackgroundScreenDefault(false)
- {
- Alpha = 0,
- };
-
- private BackgroundScreenDefault background;
-
public IntroWelcome([CanBeNull] Func createNextScreen = null)
: base(createNextScreen)
{
@@ -100,7 +91,7 @@ namespace osu.Game.Screens.Menu
logo.ScaleTo(1);
logo.FadeIn(fade_in_time);
- background.FadeIn(fade_in_time);
+ FadeInBackground(fade_in_time);
LoadMenu();
}, delay_step_two);
@@ -108,12 +99,6 @@ namespace osu.Game.Screens.Menu
}
}
- public override void OnResuming(ScreenTransitionEvent e)
- {
- base.OnResuming(e);
- background.FadeOut(100);
- }
-
private class WelcomeIntroSequence : Container
{
private Drawable welcomeText;
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index ba63902b46..066a37055c 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -65,9 +65,7 @@ namespace osu.Game.Screens.Menu
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
- private BackgroundScreenDefault background;
-
- protected override BackgroundScreen CreateBackground() => background;
+ protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
protected override bool PlayExitSound => false;
@@ -148,7 +146,6 @@ namespace osu.Game.Screens.Menu
Buttons.OnSettings = () => settings?.ToggleVisibility();
Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
- LoadComponentAsync(background = new BackgroundScreenDefault());
preloadSongSelect();
}
diff --git a/osu.Game/Screens/Select/NoResultsPlaceholder.cs b/osu.Game/Screens/Select/NoResultsPlaceholder.cs
index 5d5eafd2e6..b8b589ff99 100644
--- a/osu.Game/Screens/Select/NoResultsPlaceholder.cs
+++ b/osu.Game/Screens/Select/NoResultsPlaceholder.cs
@@ -135,7 +135,7 @@ namespace osu.Game.Screens.Select
// TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch).
// TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting.
- if (filter?.Ruleset?.OnlineID > 0 && !filter.AllowConvertedBeatmaps)
+ if (filter?.Ruleset?.OnlineID != 0 && filter?.AllowConvertedBeatmaps == false)
{
textFlow.AddParagraph("- Try");
textFlow.AddLink(" enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
diff --git a/osu.Game/Tests/FlakyTestAttribute.cs b/osu.Game/Tests/FlakyTestAttribute.cs
index 299dbb89a2..c61ce80bf5 100644
--- a/osu.Game/Tests/FlakyTestAttribute.cs
+++ b/osu.Game/Tests/FlakyTestAttribute.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Tests
}
public FlakyTestAttribute(int tryCount)
- : base(Environment.GetEnvironmentVariable("OSU_TESTS_FAIL_FLAKY") == "1" ? 0 : tryCount)
+ : base(Environment.GetEnvironmentVariable("OSU_TESTS_FAIL_FLAKY") == "1" ? 1 : tryCount)
{
}
}
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index c13cdff820..012c512266 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -430,11 +430,19 @@ namespace osu.Game.Tests.Visual
return accumulated == seek;
}
+ public override Task SeekAsync(double seek) => Task.FromResult(Seek(seek));
+
public override void Start()
{
running = true;
}
+ public override Task StartAsync()
+ {
+ Start();
+ return Task.CompletedTask;
+ }
+
public override void Reset()
{
Seek(0);
@@ -450,6 +458,12 @@ namespace osu.Game.Tests.Visual
}
}
+ public override Task StopAsync()
+ {
+ Stop();
+ return Task.CompletedTask;
+ }
+
public override bool IsRunning => running;
private double? lastReferenceTime;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index ed6577651a..06b022ea44 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7d70b96f62..9085205bbc 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -84,7 +84,7 @@
-
+