diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c728d89ed1..ef729a779f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -126,12 +126,6 @@ jobs:
with:
dotnet-version: "6.0.x"
- # macOS agents recently have empty NuGet config files, resulting in restore failures,
- # see https://github.com/actions/virtual-environments/issues/5768
- # Add the global nuget package source manually for now.
- - name: Setup NuGet.Config
- run: echo '' > ~/.config/NuGet/NuGet.Config
-
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
# cannot accept .sln(f) files as arguments.
# Build just the main game for now.
diff --git a/Gemfile.lock b/Gemfile.lock
index ddab497657..cae682ec2b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -8,20 +8,20 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
- aws-partitions (1.570.0)
- aws-sdk-core (3.130.0)
+ aws-partitions (1.601.0)
+ aws-sdk-core (3.131.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
- jmespath (~> 1.0)
- aws-sdk-kms (1.55.0)
+ jmespath (~> 1, >= 1.6.1)
+ aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.113.0)
+ aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
- aws-sigv4 (1.4.0)
+ aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
@@ -36,7 +36,7 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.3)
- excon (0.92.1)
+ excon (0.92.3)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@@ -56,8 +56,8 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
- faraday-multipart (1.0.3)
- multipart-post (>= 1.2, < 3)
+ faraday-multipart (1.0.4)
+ multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
@@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
- fastlane (2.205.1)
+ fastlane (2.206.2)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -110,9 +110,9 @@ GEM
souyuz (= 0.11.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
- google-apis-androidpublisher_v3 (0.16.0)
- google-apis-core (>= 0.4, < 2.a)
- google-apis-core (0.4.2)
+ google-apis-androidpublisher_v3 (0.23.0)
+ google-apis-core (>= 0.6, < 2.a)
+ google-apis-core (0.6.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -121,19 +121,19 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
- google-apis-iamcredentials_v1 (0.10.0)
- google-apis-core (>= 0.4, < 2.a)
- google-apis-playcustomapp_v1 (0.7.0)
- google-apis-core (>= 0.4, < 2.a)
- google-apis-storage_v1 (0.11.0)
- google-apis-core (>= 0.4, < 2.a)
+ google-apis-iamcredentials_v1 (0.12.0)
+ google-apis-core (>= 0.6, < 2.a)
+ google-apis-playcustomapp_v1 (0.9.0)
+ google-apis-core (>= 0.6, < 2.a)
+ google-apis-storage_v1 (0.16.0)
+ google-apis-core (>= 0.6, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
- google-cloud-storage (1.36.1)
+ google-cloud-storage (1.36.2)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@@ -141,7 +141,7 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
- googleauth (1.1.2)
+ googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -149,12 +149,12 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
- http-cookie (1.0.4)
+ http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.1)
- json (2.6.1)
- jwt (2.3.0)
+ json (2.6.2)
+ jwt (2.4.1)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
@@ -169,10 +169,10 @@ GEM
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
- public_suffix (4.0.6)
+ public_suffix (4.0.7)
racc (1.6.0)
rake (13.0.6)
- representable (3.1.1)
+ representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
@@ -182,9 +182,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
- signet (0.16.1)
+ signet (0.17.0)
addressable (~> 2.8)
- faraday (>= 0.17.5, < 3.0)
+ faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@@ -205,11 +205,11 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.8.1)
+ unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
- xcodeproj (1.21.0)
+ xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index bc285dbe11..011a37cbdc 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -9,7 +9,6 @@
false
-
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 718ada1905..c04f6132f3 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,7 +9,6 @@
false
-
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index 6b9c3f4d63..529054fd4f 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -9,7 +9,6 @@
false
-
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 718ada1905..c04f6132f3 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,7 +9,6 @@
false
-
diff --git a/osu.Android.props b/osu.Android.props
index 8f4a102cce..d5390e6a3d 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 712f300671..cebbcb40b7 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -14,6 +14,7 @@ using osu.Framework.Platform;
using osu.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
+using SDL2;
using Squirrel;
namespace osu.Desktop
@@ -29,7 +30,21 @@ namespace osu.Desktop
{
// run Squirrel first, as the app may exit after these run
if (OperatingSystem.IsWindows())
+ {
+ var windowsVersion = Environment.OSVersion.Version;
+
+ // While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
+ // See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
+ if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
+ {
+ SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
+ "Your operating system is too old to run osu!",
+ "This version of osu! requires at least Windows 8.1 to run.\nPlease upgrade your operating system or consider using an older version of osu!.", IntPtr.Zero);
+ return;
+ }
+
setupSquirrel();
+ }
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index a4f9e2671b..c67017f175 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
index 07ffda4030..1d207d04c7 100644
--- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
+++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.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.IO;
using BenchmarkDotNet.Attributes;
using osu.Framework.IO.Stores;
diff --git a/osu.Game.Benchmarks/BenchmarkMod.cs b/osu.Game.Benchmarks/BenchmarkMod.cs
index a1d92d9a67..994300df36 100644
--- a/osu.Game.Benchmarks/BenchmarkMod.cs
+++ b/osu.Game.Benchmarks/BenchmarkMod.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 BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Osu.Mods;
@@ -11,7 +9,7 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkMod : BenchmarkTest
{
- private OsuModDoubleTime mod;
+ private OsuModDoubleTime mod = null!;
[Params(1, 10, 100)]
public int Times { get; set; }
diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs
index 5ffda6504e..1df77320d2 100644
--- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs
+++ b/osu.Game.Benchmarks/BenchmarkRealmReads.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.Linq;
using System.Threading;
using BenchmarkDotNet.Attributes;
@@ -17,9 +15,9 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkRealmReads : BenchmarkTest
{
- private TemporaryNativeStorage storage;
- private RealmAccess realm;
- private UpdateThread updateThread;
+ private TemporaryNativeStorage storage = null!;
+ private RealmAccess realm = null!;
+ private UpdateThread updateThread = null!;
[Params(1, 100, 1000)]
public int ReadsPerFetch { get; set; }
@@ -135,9 +133,9 @@ namespace osu.Game.Benchmarks
[GlobalCleanup]
public void Cleanup()
{
- realm?.Dispose();
- storage?.Dispose();
- updateThread?.Exit();
+ realm.Dispose();
+ storage.Dispose();
+ updateThread.Exit();
}
}
}
diff --git a/osu.Game.Benchmarks/BenchmarkRuleset.cs b/osu.Game.Benchmarks/BenchmarkRuleset.cs
index de8cb13773..7d318e043b 100644
--- a/osu.Game.Benchmarks/BenchmarkRuleset.cs
+++ b/osu.Game.Benchmarks/BenchmarkRuleset.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 BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using osu.Game.Online.API;
@@ -13,9 +11,9 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkRuleset : BenchmarkTest
{
- private OsuRuleset ruleset;
- private APIMod apiModDoubleTime;
- private APIMod apiModDifficultyAdjust;
+ private OsuRuleset ruleset = null!;
+ private APIMod apiModDoubleTime = null!;
+ private APIMod apiModDifficultyAdjust = null!;
public override void SetUp()
{
diff --git a/osu.Game.Benchmarks/BenchmarkTest.cs b/osu.Game.Benchmarks/BenchmarkTest.cs
index 140696e4a4..34f5edd084 100644
--- a/osu.Game.Benchmarks/BenchmarkTest.cs
+++ b/osu.Game.Benchmarks/BenchmarkTest.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 BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using NUnit.Framework;
diff --git a/osu.Game.Benchmarks/Program.cs b/osu.Game.Benchmarks/Program.cs
index 603d8aa1b9..439ced53ab 100644
--- a/osu.Game.Benchmarks/Program.cs
+++ b/osu.Game.Benchmarks/Program.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 BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs
index c65c9df9f9..b9d6f28228 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs
@@ -24,21 +24,24 @@ namespace osu.Game.Rulesets.Catch.Tests
new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
- new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
- new object[] { LegacyMods.Perfect, new[] { typeof(CatchModPerfect) } },
- new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
};
+ [TestCaseSource(nameof(catch_mod_mapping))]
+ [TestCase(LegacyMods.Cinema, new[] { typeof(CatchModCinema) })]
+ [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
+ [TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })]
+ [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
+ [TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
+ public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
+
[TestCaseSource(nameof(catch_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
- public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
-
- [TestCaseSource(nameof(catch_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new CatchRuleset();
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
index 731cb4e135..8dd6f82c57 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("change component scale", () => Player.ChildrenOfType().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit());
- CreateTest(null);
+ CreateTest();
}
AddAssert("legacy HUD combo counter hidden", () =>
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index b957ade952..3ac1491946 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -1,7 +1,6 @@
-
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
index 9951d736c3..2d01153f98 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs
@@ -1,10 +1,9 @@
// 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 Newtonsoft.Json;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch.Difficulty
@@ -31,9 +30,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
}
- public override void FromDatabaseAttributes(IReadOnlyDictionary values)
+ public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
{
- base.FromDatabaseAttributes(values);
+ base.FromDatabaseAttributes(values, onlineInfo);
StarRating = values[ATTRIB_ID_AIM];
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 86c249a7c1..7c84cb24f3 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.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 osu.Framework.Utils;
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
index 64a573149f..b6af88a771 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.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 osu.Framework.Input.StateChanges;
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
index 5cf03e4706..e30e535e9b 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.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 osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
@@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Replays
{
}
- public CatchReplayFrame(double time, float? position = null, bool dashing = false, CatchReplayFrame lastFrame = null)
+ public CatchReplayFrame(double time, float? position = null, bool dashing = false, CatchReplayFrame? lastFrame = null)
: base(time)
{
Position = position ?? -1;
@@ -40,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
- public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
+ public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
{
Position = currentFrame.Position.X;
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index a0279b5c83..7a29ba9801 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -120,10 +120,10 @@ namespace osu.Game.Rulesets.Catch.UI
lastHyperDashState = Catcher.HyperDashing;
}
- public void SetCatcherPosition(float X)
+ public void SetCatcherPosition(float x)
{
float lastPosition = Catcher.X;
- float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
+ float newPosition = Math.Clamp(x, 0, CatchPlayfield.WIDTH);
Catcher.X = newPosition;
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
index accae29ffe..9dee861e66 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs
@@ -23,10 +23,8 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } },
new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } },
- new object[] { LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } },
- new object[] { LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) } },
new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } },
new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } },
new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } },
@@ -34,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } },
new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } },
new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } },
- new object[] { LegacyMods.Cinema, new[] { typeof(ManiaModCinema) } },
new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } },
new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } },
new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } },
@@ -45,12 +42,18 @@ namespace osu.Game.Rulesets.Mania.Tests
};
[TestCaseSource(nameof(mania_mod_mapping))]
+ [TestCase(LegacyMods.Cinema, new[] { typeof(ManiaModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
+ [TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
+ [TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(mania_mod_mapping))]
+ [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
+ [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index d3b4b378c0..d07df75864 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -1,7 +1,6 @@
-
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
index 8a5161be79..d259c2af8e 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs
@@ -1,10 +1,9 @@
// 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 Newtonsoft.Json;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Mania.Difficulty
@@ -20,12 +19,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
[JsonProperty("great_hit_window")]
public double GreatHitWindow { get; set; }
- ///
- /// The score multiplier applied via score-reducing mods.
- ///
- [JsonProperty("score_multiplier")]
- public double ScoreMultiplier { get; set; }
-
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{
foreach (var v in base.ToDatabaseAttributes())
@@ -34,17 +27,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
- yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
}
- public override void FromDatabaseAttributes(IReadOnlyDictionary values)
+ public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
{
- base.FromDatabaseAttributes(values);
+ base.FromDatabaseAttributes(values, onlineInfo);
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
- ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index 979aaa1cf5..178094476f 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
- ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
};
}
@@ -147,32 +146,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return value;
}
}
-
- private double getScoreMultiplier(Mod[] mods)
- {
- double scoreMultiplier = 1;
-
- foreach (var m in mods)
- {
- switch (m)
- {
- case ManiaModNoFail:
- case ManiaModEasy:
- case ManiaModHalfTime:
- scoreMultiplier *= 0.5;
- break;
- }
- }
-
- var maniaBeatmap = (ManiaBeatmap)Beatmap;
- int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
-
- if (diff > 0)
- scoreMultiplier *= 0.9;
- else if (diff < 0)
- scoreMultiplier *= 0.9 + 0.04 * diff;
-
- return scoreMultiplier;
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs
index f5abb465c4..01474e6e00 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceAttributes.cs
@@ -14,19 +14,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
[JsonProperty("difficulty")]
public double Difficulty { get; set; }
- [JsonProperty("accuracy")]
- public double Accuracy { get; set; }
-
- [JsonProperty("scaled_score")]
- public double ScaledScore { get; set; }
-
public override IEnumerable GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
yield return attribute;
yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty);
- yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index eb58eb7f21..a925e7c0ac 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -15,15 +15,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaPerformanceCalculator : PerformanceCalculator
{
- // Score after being scaled by non-difficulty-increasing mods
- private double scaledScore;
-
private int countPerfect;
private int countGreat;
private int countGood;
private int countOk;
private int countMeh;
private int countMiss;
+ private double scoreAccuracy;
public ManiaPerformanceCalculator()
: base(new ManiaRuleset())
@@ -34,82 +32,47 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
var maniaAttributes = (ManiaDifficultyAttributes)attributes;
- scaledScore = score.TotalScore;
countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
-
- if (maniaAttributes.ScoreMultiplier > 0)
- {
- // Scale score up, so it's comparable to other keymods
- scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier;
- }
+ scoreAccuracy = customAccuracy;
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed.
- double multiplier = 0.8;
+ double multiplier = 8.0;
if (score.Mods.Any(m => m is ModNoFail))
- multiplier *= 0.9;
+ multiplier *= 0.75;
if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.5;
double difficultyValue = computeDifficultyValue(maniaAttributes);
- double accValue = computeAccuracyValue(difficultyValue, maniaAttributes);
- double totalValue =
- Math.Pow(
- Math.Pow(difficultyValue, 1.1) +
- Math.Pow(accValue, 1.1), 1.0 / 1.1
- ) * multiplier;
+ double totalValue = difficultyValue * multiplier;
return new ManiaPerformanceAttributes
{
Difficulty = difficultyValue,
- Accuracy = accValue,
- ScaledScore = scaledScore,
Total = totalValue
};
}
private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
{
- double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
-
- difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
-
- if (scaledScore <= 500000)
- difficultyValue = 0;
- else if (scaledScore <= 600000)
- difficultyValue *= (scaledScore - 500000) / 100000 * 0.3;
- else if (scaledScore <= 700000)
- difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
- else if (scaledScore <= 800000)
- difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
- else if (scaledScore <= 900000)
- difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
- else
- difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
+ double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
+ * Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
+ * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
return difficultyValue;
}
- private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes)
- {
- if (attributes.GreatHitWindow <= 0)
- return 0;
-
- // Lots of arbitrary values from testing.
- // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
- double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667)
- * difficultyValue
- * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
-
- return accuracyValue;
- }
-
private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss;
+
+ ///
+ /// Accuracy used to weight judgements independently from the score's actual accuracy.
+ ///
+ private double customAccuracy => (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
index 58aef4dbf8..c8832dfdfb 100644
--- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs
+++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.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.Beatmaps;
using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps;
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index 26572de412..7c8afdff12 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -1,10 +1,9 @@
// 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 osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
@@ -85,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Replays
}
}
- private double calculateReleaseTime(HitObject currentObject, HitObject nextObject)
+ private double calculateReleaseTime(HitObject currentObject, HitObject? nextObject)
{
double endTime = currentObject.GetEndTime();
@@ -96,10 +95,10 @@ namespace osu.Game.Rulesets.Mania.Replays
bool canDelayKeyUpFully = nextObject == null ||
nextObject.StartTime > endTime + RELEASE_DELAY;
- return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.StartTime - endTime) * 0.9);
+ return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.AsNonNull().StartTime - endTime) * 0.9);
}
- protected override HitObject GetNextObject(int currentIndex)
+ protected override HitObject? GetNextObject(int currentIndex)
{
int desiredColumn = Beatmap.HitObjects[currentIndex].Column;
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs
index c786e1db61..aa164f95da 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.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 osu.Framework.Input.StateChanges;
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
index 66edc87992..29249ba474 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.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 osu.Game.Beatmaps;
@@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Replays
Actions.AddRange(actions);
}
- public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
+ public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs
index 3d9fe37e0f..ef9e332253 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs
@@ -10,7 +10,6 @@ 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;
@@ -41,10 +40,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
- private bool editorComponentsReady => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true
- && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true
- && editor?.ChildrenOfType().FirstOrDefault()?.IsLoaded == true;
-
[TestCase(true)]
[TestCase(false)]
public void TestVelocityChangeSavesCorrectly(bool adjustVelocity)
@@ -52,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
double? velocity = null;
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
- AddUntilStep("wait for editor load", () => editorComponentsReady);
+ AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time));
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
@@ -91,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("exit", () => InputManager.Key(Key.Escape));
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
- AddUntilStep("wait for editor load", () => editorComponentsReady);
+ AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
AddStep("seek to slider", () => editorClock.Seek(slider.StartTime));
AddAssert("slider has correct velocity", () => slider.Velocity == velocity);
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.cs
new file mode 100644
index 0000000000..6bd41e2fa5
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModRepel : OsuModTestScene
+ {
+ [TestCase(0.1f)]
+ [TestCase(0.5f)]
+ [TestCase(1)]
+ public void TestRepel(float strength)
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModRepel
+ {
+ RepulsionStrength = { Value = strength },
+ },
+ PassCondition = () => true,
+ Autoplay = false,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
new file mode 100644
index 0000000000..1aed84be10
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
@@ -0,0 +1,175 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModSingleTap : OsuModTestScene
+ {
+ [Test]
+ public void TestInputSingular() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 1500,
+ Position = new Vector2(300, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 2000,
+ Position = new Vector2(400, 100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
+ }
+ });
+
+ [Test]
+ public void TestInputAlternating() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
+ new OsuReplayFrame(1001, new Vector2(200, 100)),
+ new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(1501, new Vector2(300, 100)),
+ new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
+ new OsuReplayFrame(2001, new Vector2(400, 100)),
+ }
+ });
+
+ ///
+ /// Ensures singletapping is reset before the first hitobject after intro.
+ ///
+ [Test]
+ public void TestInputAlternatingAtIntro() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ // first press during intro.
+ new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(200)),
+ // press different key at hitobject and ensure it has been hit.
+ new OsuReplayFrame(1000, new Vector2(100), OsuAction.RightButton),
+ }
+ });
+
+ ///
+ /// Ensures singletapping is reset before the first hitobject after a break.
+ ///
+ [Test]
+ public void TestInputAlternatingWithBreak() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ Breaks = new List
+ {
+ new BreakPeriod(500, 2000),
+ },
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 2500,
+ Position = new Vector2(500, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(500, 100),
+ },
+ }
+ },
+ ReplayFrames = new List
+ {
+ // first press to start singletap lock.
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ // press different key after break but before hit object.
+ new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.RightButton),
+ new OsuReplayFrame(2251, new Vector2(300, 100)),
+ // press same key at second hitobject and ensure it has been hit.
+ new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2501, new Vector2(500, 100)),
+ // press different key at third hitobject and ensure it has been missed.
+ new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.RightButton),
+ new OsuReplayFrame(3001, new Vector2(500, 100)),
+ }
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs
index db9872b152..01d83b55e6 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs
@@ -25,24 +25,27 @@ namespace osu.Game.Rulesets.Osu.Tests
new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } },
- new object[] { LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } },
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
- new object[] { LegacyMods.Perfect, new[] { typeof(OsuModPerfect) } },
- new object[] { LegacyMods.Cinema, new[] { typeof(OsuModCinema) } },
new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
};
[TestCaseSource(nameof(osu_mod_mapping))]
+ [TestCase(LegacyMods.Cinema, new[] { typeof(OsuModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
+ [TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
+ [TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(osu_mod_mapping))]
+ [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
+ [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new OsuRuleset();
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
index 2981f1164d..08a62fe3ae 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
@@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
@@ -93,10 +92,10 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
- AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White);
+ AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
- AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red);
+ AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red);
}
private Slider prepareObject(Slider slider)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs
new file mode 100644
index 0000000000..7a6e19575e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs
@@ -0,0 +1,120 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [HeadlessTest]
+ public class TestSceneSliderFollowCircleInput : RateAdjustedBeatmapTestScene
+ {
+ private List? judgementResults;
+ private ScoreAccessibleReplayPlayer? currentPlayer;
+
+ [Test]
+ public void TestMaximumDistanceTrackingWithoutMovement(
+ [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
+ float circleSize,
+ [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
+ double velocity)
+ {
+ const double time_slider_start = 1000;
+
+ float circleRadius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (circleSize - 5) / 5) / 2;
+ float followCircleRadius = circleRadius * 1.2f;
+
+ performTest(new Beatmap
+ {
+ HitObjects =
+ {
+ new Slider
+ {
+ StartTime = time_slider_start,
+ Position = new Vector2(0, 0),
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity },
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(followCircleRadius, 0),
+ }, followCircleRadius),
+ },
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty
+ {
+ CircleSize = circleSize,
+ SliderTickRate = 1
+ },
+ Ruleset = new OsuRuleset().RulesetInfo
+ },
+ }, new List
+ {
+ new OsuReplayFrame { Position = new Vector2(-circleRadius + 1, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
+ });
+
+ AddAssert("Tracking kept", assertMaxJudge);
+ }
+
+ private bool assertMaxJudge() => judgementResults?.Any() == true && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
+
+ private void performTest(Beatmap beatmap, List frames)
+ {
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(beatmap);
+
+ var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
+
+ p.OnLoadComplete += _ =>
+ {
+ p.ScoreProcessor.NewJudgement += result =>
+ {
+ if (currentPlayer == p) judgementResults?.Add(result);
+ };
+ };
+
+ LoadScreen(currentPlayer = p);
+ judgementResults = new List();
+ });
+
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for completion", () => currentPlayer?.ScoreProcessor.HasCompleted.Value == true);
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ protected override bool PauseOnFocusLost => false;
+
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score, new PlayerConfiguration
+ {
+ AllowPause = false,
+ ShowResults = false,
+ })
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
index 5706955fc5..0118ed6513 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
@@ -24,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Storyboards;
+using osu.Game.Tests;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@@ -66,18 +67,25 @@ namespace osu.Game.Rulesets.Osu.Tests
drawableSlider = null;
});
- [SetUpSteps]
- public override void SetUpSteps()
- {
- }
+ protected override bool HasCustomSteps => true;
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
+ [FlakyTest]
+ /*
+ * Fail rate around 0.15%
+ *
+ * TearDown : System.TimeoutException : "wait for seek to finish" timed out
+ * --TearDown
+ * at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
+ * at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
+ * at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
+ */
public void TestSnakingEnabled(int sliderIndex)
{
AddStep("enable autoplay", () => autoplay = true);
- base.SetUpSteps();
+ CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
retrieveSlider(sliderIndex);
@@ -98,10 +106,20 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
+ [FlakyTest]
+ /*
+ * Fail rate around 0.15%
+ *
+ * TearDown : System.TimeoutException : "wait for seek to finish" timed out
+ * --TearDown
+ * at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
+ * at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
+ * at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
+ */
public void TestSnakingDisabled(int sliderIndex)
{
AddStep("have autoplay", () => autoplay = true);
- base.SetUpSteps();
+ CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
retrieveSlider(sliderIndex);
@@ -121,8 +139,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("enable autoplay", () => autoplay = true);
setSnaking(true);
- base.SetUpSteps();
-
+ CreateTest();
// repeat might have a chance to update its position depending on where in the frame its hit,
// so some leniency is allowed here instead of checking strict equality
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
@@ -133,15 +150,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep("disable autoplay", () => autoplay = false);
setSnaking(true);
- base.SetUpSteps();
-
+ CreateTest();
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
}
private void retrieveSlider(int index)
{
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
- addSeekStep(() => slider);
+ addSeekStep(() => slider.StartTime);
AddUntilStep("retrieve drawable slider", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
}
@@ -205,16 +221,10 @@ namespace osu.Game.Rulesets.Osu.Tests
});
}
- private void addSeekStep(Func slider)
+ private void addSeekStep(Func getTime)
{
- AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime));
- AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
- }
-
- private void addSeekStep(Func time)
- {
- AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
- AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ AddStep("seek to time", () => Player.GameplayClockContainer.Seek(getTime()));
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(getTime(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 2c0d3fd937..4349d25cb3 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -1,9 +1,8 @@
-
-
+
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index 19217015c1..03540abddb 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -1,12 +1,11 @@
// 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 JetBrains.Annotations;
using Newtonsoft.Json;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@@ -26,6 +25,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("speed_difficulty")]
public double SpeedDifficulty { get; set; }
+ ///
+ /// The number of clickable objects weighted by difficulty.
+ /// Related to
+ ///
+ [JsonProperty("speed_note_count")]
+ public double SpeedNoteCount { get; set; }
+
///
/// The difficulty corresponding to the flashlight skill.
///
@@ -94,11 +100,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
+ yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
}
- public override void FromDatabaseAttributes(IReadOnlyDictionary values)
+ public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
{
- base.FromDatabaseAttributes(values);
+ base.FromDatabaseAttributes(values, onlineInfo);
AimDifficulty = values[ATTRIB_ID_AIM];
SpeedDifficulty = values[ATTRIB_ID_SPEED];
@@ -108,6 +115,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
StarRating = values[ATTRIB_ID_DIFFICULTY];
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
+ SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
+
+ DrainRate = onlineInfo.DrainRate;
+ HitCircleCount = onlineInfo.CircleCount;
+ SliderCount = onlineInfo.SliderCount;
+ SpinnerCount = onlineInfo.SpinnerCount;
}
#region Newtonsoft.Json implicit ShouldSerialize() methods
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 3b5aaa8116..75d9469da3 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
+ double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
@@ -75,6 +76,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Mods = mods,
AimDifficulty = aimRating,
SpeedDifficulty = speedRating,
+ SpeedNoteCount = speedNotes,
FlashlightDifficulty = flashlightRating,
SliderFactor = sliderFactor,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 548a2b8f8a..c3b7834009 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -163,8 +163,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
+ // Calculate accuracy assuming the worst case scenario
+ double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
+ double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
+ double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
+ double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
+ double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
+
// Scale the speed value with accuracy and OD.
- speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
+ speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
index 14d13ec785..84ef109598 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
@@ -6,6 +6,7 @@
using System;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Mods;
@@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
/// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled.
///
- public class Flashlight : OsuStrainSkill
+ public class Flashlight : StrainSkill
{
private readonly bool hasHiddenMod;
@@ -27,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double skillMultiplier => 0.05;
private double strainDecayBase => 0.15;
- protected override double DecayWeight => 1.0;
private double currentStrain;
@@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return currentStrain;
}
+
+ public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
index 94b5727e3f..d6683ade05 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -14,6 +14,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
public abstract class OsuStrainSkill : StrainSkill
{
+ ///
+ /// The default multiplier applied by to the final difficulty value after all other calculations.
+ /// May be overridden via .
+ ///
+ public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06;
+
///
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
@@ -28,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
/// The final multiplier to be applied to after all other calculations.
///
- protected virtual double DifficultyMultiplier => 1.06;
+ protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER;
protected OsuStrainSkill(Mod[] mods)
: base(mods)
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index fffa886dd0..a156726f94 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -8,6 +8,8 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
+using System.Collections.Generic;
+using System.Linq;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
@@ -26,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double DifficultyMultiplier => 1.04;
private readonly double greatWindow;
+ private readonly List objectStrains = new List();
+
public Speed(Mod[] mods, double hitWindowGreat)
: base(mods)
{
@@ -43,7 +47,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow);
- return currentStrain * currentRhythm;
+ double totalStrain = currentStrain * currentRhythm;
+
+ objectStrains.Add(totalStrain);
+
+ return totalStrain;
+ }
+
+ public double RelevantNoteCount()
+ {
+ if (objectStrains.Count == 0)
+ return 0;
+
+ double maxStrain = objectStrains.Max();
+
+ if (maxStrain == 0)
+ return 0;
+
+ return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0)))));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
new file mode 100644
index 0000000000..a7aca8257b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
+using osu.Game.Utils;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset
+ {
+ public override double ScoreMultiplier => 1.0;
+ public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
+ public override ModType Type => ModType.Conversion;
+
+ private const double flash_duration = 1000;
+
+ private DrawableRuleset ruleset = null!;
+
+ protected OsuAction? LastAcceptedAction { get; private set; }
+
+ ///
+ /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
+ ///
+ ///
+ /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time.
+ ///
+ private PeriodTracker nonGameplayPeriods = null!;
+
+ private IFrameStableClock gameplayClock = null!;
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ ruleset = drawableRuleset;
+ drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
+
+ var periods = new List();
+
+ if (drawableRuleset.Objects.Any())
+ {
+ periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
+
+ foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
+ periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
+
+ static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
+ }
+
+ nonGameplayPeriods = new PeriodTracker(periods);
+
+ gameplayClock = drawableRuleset.FrameStableClock;
+ }
+
+ protected abstract bool CheckValidNewAction(OsuAction action);
+
+ private bool checkCorrectAction(OsuAction action)
+ {
+ if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
+ {
+ LastAcceptedAction = null;
+ return true;
+ }
+
+ switch (action)
+ {
+ case OsuAction.LeftButton:
+ case OsuAction.RightButton:
+ break;
+
+ // Any action which is not left or right button should be ignored.
+ default:
+ return true;
+ }
+
+ if (CheckValidNewAction(action))
+ {
+ LastAcceptedAction = action;
+ return true;
+ }
+
+ ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
+ return false;
+ }
+
+ private class InputInterceptor : Component, IKeyBindingHandler
+ {
+ private readonly InputBlockingMod mod;
+
+ public InputInterceptor(InputBlockingMod mod)
+ {
+ this.mod = mod;
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ // if the pressed action is incorrect, block it from reaching gameplay.
+ => !mod.checkCorrectAction(e.Action);
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
index 622d2df432..d88cb17e84 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
@@ -1,119 +1,20 @@
// 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;
-using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Input.Events;
-using osu.Game.Beatmaps.Timing;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Play;
-using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModAlternate : Mod, IApplicableToDrawableRuleset
+ public class OsuModAlternate : InputBlockingMod
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
- public override double ScoreMultiplier => 1.0;
- public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
- public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
- private const double flash_duration = 1000;
-
- ///
- /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
- ///
- ///
- /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time.
- ///
- private PeriodTracker nonGameplayPeriods;
-
- private OsuAction? lastActionPressed;
- private DrawableRuleset ruleset;
-
- private IFrameStableClock gameplayClock;
-
- public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
- {
- ruleset = drawableRuleset;
- drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
-
- var periods = new List();
-
- if (drawableRuleset.Objects.Any())
- {
- periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
-
- foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
- periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
-
- static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
- }
-
- nonGameplayPeriods = new PeriodTracker(periods);
-
- gameplayClock = drawableRuleset.FrameStableClock;
- }
-
- private bool checkCorrectAction(OsuAction action)
- {
- if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
- {
- lastActionPressed = null;
- return true;
- }
-
- switch (action)
- {
- case OsuAction.LeftButton:
- case OsuAction.RightButton:
- break;
-
- // Any action which is not left or right button should be ignored.
- default:
- return true;
- }
-
- if (lastActionPressed != action)
- {
- // User alternated correctly.
- lastActionPressed = action;
- return true;
- }
-
- ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
- return false;
- }
-
- private class InputInterceptor : Component, IKeyBindingHandler
- {
- private readonly OsuModAlternate mod;
-
- public InputInterceptor(OsuModAlternate mod)
- {
- this.mod = mod;
- }
-
- public bool OnPressed(KeyBindingPressEvent e)
- // if the pressed action is incorrect, block it from reaching gameplay.
- => !mod.checkCorrectAction(e.Action);
-
- public void OnReleased(KeyBindingReleaseEvent e)
- {
- }
- }
+ protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 4c9418726c..a3f6448457 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public bool PerformFail() => false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index d562c37541..c4de77b8a3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index 656cf95e77..704b922ee5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
index 8a15d730cd..00009f4c3d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
@@ -30,9 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
public Bindable ClassicNoteLock { get; } = new BindableBool(true);
- [SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
- public Bindable FixedFollowCircleHitArea { get; } = new BindableBool(true);
-
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
public Bindable AlwaysPlayTailSample { get; } = new BindableBool(true);
@@ -62,10 +59,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
switch (obj)
{
- case DrawableSlider slider:
- slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
- break;
-
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index cee40866b1..f9a74d2a3a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 2cf8c278ca..2030156f2e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
///
/// How early before a hitobject's start time to trigger a hit.
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
new file mode 100644
index 0000000000..211987ee32
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
@@ -0,0 +1,98 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using osu.Framework.Bindables;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+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.Osu.UI;
+using osu.Game.Rulesets.Osu.Utils;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ internal class OsuModRepel : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset
+ {
+ public override string Name => "Repel";
+ public override string Acronym => "RP";
+ public override ModType Type => ModType.Fun;
+ public override string Description => "Hit objects run away!";
+ public override double ScoreMultiplier => 1;
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
+
+ private IFrameStableClock? gameplayClock;
+
+ [SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
+ public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
+ {
+ Precision = 0.05f,
+ MinValue = 0.05f,
+ MaxValue = 1.0f,
+ };
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ gameplayClock = drawableRuleset.FrameStableClock;
+
+ // Hide judgment displays and follow points as they won't make any sense.
+ // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
+ drawableRuleset.Playfield.DisplayJudgements.Value = false;
+ (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
+ }
+
+ public void Update(Playfield playfield)
+ {
+ var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
+
+ foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
+ {
+ var destination = Vector2.Clamp(2 * drawable.Position - cursorPos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
+
+ if (drawable.HitObject is Slider thisSlider)
+ {
+ var possibleMovementBounds = OsuHitObjectGenerationUtils.CalculatePossibleMovementBounds(thisSlider);
+
+ destination = Vector2.Clamp(
+ destination,
+ new Vector2(possibleMovementBounds.Left, possibleMovementBounds.Top),
+ new Vector2(possibleMovementBounds.Right, possibleMovementBounds.Bottom)
+ );
+ }
+
+ switch (drawable)
+ {
+ case DrawableHitCircle circle:
+ easeTo(circle, destination, cursorPos);
+ break;
+
+ case DrawableSlider slider:
+
+ if (!slider.HeadCircle.Result.HasResult)
+ easeTo(slider, destination, cursorPos);
+ else
+ easeTo(slider, destination - slider.Ball.DrawPosition, cursorPos);
+
+ break;
+ }
+ }
+ }
+
+ private void easeTo(DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
+ {
+ Debug.Assert(gameplayClock != null);
+
+ double dampLength = Vector2.Distance(hitObject.Position, cursorPos) / (0.04 * RepulsionStrength.Value + 0.04);
+
+ float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
+ float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
+
+ hitObject.Position = new Vector2(x, y);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
new file mode 100644
index 0000000000..051ceb968c
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModSingleTap : InputBlockingMod
+ {
+ public override string Name => @"Single Tap";
+ public override string Acronym => @"ST";
+ public override string Description => @"You must only use one key!";
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
+
+ protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index 5a08df3803..84906f6eed 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private float theta;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index 3fba2cefd2..8acd4fc422 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 9e3b762690..d83f5df7a3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -29,7 +29,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSliderHead HeadCircle => headContainer.Child;
public DrawableSliderTail TailCircle => tailContainer.Child;
- public SliderBall Ball { get; private set; }
+ [Cached]
+ public DrawableSliderBall Ball { get; private set; }
+
public SkinnableDrawable Body { get; private set; }
///
@@ -60,6 +62,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSlider([CanBeNull] Slider s = null)
: base(s)
{
+ Ball = new DrawableSliderBall
+ {
+ GetInitialHitAction = () => HeadCircle.HitAction,
+ BypassAutoSizeAxes = Axes.Both,
+ AlwaysPresent = true,
+ Alpha = 0
+ };
}
[BackgroundDependencyLoader]
@@ -73,13 +82,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
headContainer = new Container { RelativeSizeAxes = Axes.Both },
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
- Ball = new SliderBall(this)
- {
- GetInitialHitAction = () => HeadCircle.HitAction,
- BypassAutoSizeAxes = Axes.Both,
- AlwaysPresent = true,
- Alpha = 0
- },
+ Ball,
slidingSample = new PausableSkinnableSound { Looping = true }
};
@@ -316,13 +319,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
const float fade_out_time = 450;
- // intentionally pile on an extra FadeOut to make it happen much faster.
- Ball.FadeOut(fade_out_time / 4, Easing.Out);
-
switch (state)
{
case ArmedState.Hit:
- Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
similarity index 64%
rename from osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
rename to osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
index d2ea8f1660..6bfb4e8aae 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
@@ -7,25 +7,24 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Osu.Skinning.Default
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
+ public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
{
+ public const float FOLLOW_AREA = 2.4f;
+
public Func GetInitialHitAction;
public Color4 AccentColour
@@ -34,19 +33,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
set => ball.Colour = value;
}
- ///
- /// Whether to track accurately to the visual size of this .
- /// If false, tracking will be performed at the final scale at all times.
- ///
- public bool InputTracksVisualSize = true;
+ private Drawable followCircleReceptor;
+ private DrawableSlider drawableSlider;
+ private Drawable ball;
- private readonly Drawable followCircle;
- private readonly DrawableSlider drawableSlider;
- private readonly Drawable ball;
-
- public SliderBall(DrawableSlider drawableSlider)
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableSlider)
{
- this.drawableSlider = drawableSlider;
+ this.drawableSlider = (DrawableSlider)drawableSlider;
Origin = Anchor.Centre;
@@ -54,13 +48,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Children = new[]
{
- followCircle = new FollowCircleContainer
+ new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
+ },
+ followCircleReceptor = new CircularContainer
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true
},
ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
{
@@ -104,15 +103,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
tracking = value;
- if (InputTracksVisualSize)
- followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
- else
- {
- // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration.
- followCircle.ScaleTo(tracking ? 2.4f : 1f);
- }
-
- followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
+ followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f);
}
}
@@ -170,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
// in valid time range
Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime &&
// in valid position range
- lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
+ lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
// valid action
(actions?.Any(isValidTrackingAction) ?? false);
@@ -208,74 +199,5 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
lastPosition = Position;
}
-
- private class FollowCircleContainer : CircularContainer
- {
- public override bool HandlePositionalInput => true;
- }
-
- public class DefaultFollowCircle : CompositeDrawable
- {
- public DefaultFollowCircle()
- {
- RelativeSizeAxes = Axes.Both;
-
- InternalChild = new CircularContainer
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- BorderThickness = 5,
- BorderColour = Color4.Orange,
- Blending = BlendingParameters.Additive,
- Child = new Box
- {
- Colour = Color4.Orange,
- RelativeSizeAxes = Axes.Both,
- Alpha = 0.2f,
- }
- };
- }
- }
-
- public class DefaultSliderBall : CompositeDrawable
- {
- private Box box;
-
- [BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject, ISkinSource skin)
- {
- var slider = (DrawableSlider)drawableObject;
-
- RelativeSizeAxes = Axes.Both;
-
- float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
-
- InternalChild = new CircularContainer
- {
- Masking = true,
- RelativeSizeAxes = Axes.Both,
- Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Blending = BlendingParameters.Additive,
- BorderThickness = 10,
- BorderColour = Color4.White,
- Alpha = 1,
- Child = box = new Box
- {
- Blending = BlendingParameters.Additive,
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
- AlwaysPresent = true,
- Alpha = 0
- }
- };
-
- slider.Tracking.BindValueChanged(trackingChanged, true);
- }
-
- private void trackingChanged(ValueChangedEvent tracking) =>
- box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 120ce32612..302194e91a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(),
new OsuModRandom(),
new OsuModMirror(),
- new OsuModAlternate(),
+ new MultiMod(new OsuModAlternate(), new OsuModSingleTap())
};
case ModType.Automation:
@@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
- new OsuModMagnetised(),
+ new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed()
};
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index b0155c02cf..5a3d882ef0 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.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 osuTK;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@@ -95,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Replays
{
double endTime = prev.GetEndTime();
- HitWindows hitWindows = null;
+ HitWindows? hitWindows = null;
switch (h)
{
@@ -245,7 +243,7 @@ namespace osu.Game.Rulesets.Osu.Replays
}
double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
- OsuReplayFrame lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
+ OsuReplayFrame? lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
if (timeDifference > 0)
{
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index b41d123380..1cb3208c30 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.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 osuTK;
using osu.Game.Beatmaps;
using System;
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
index 8857bfa32d..ea36ecc399 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.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 osu.Framework.Input.StateChanges;
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
index 019d8035ed..85060261fe 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.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 osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
@@ -28,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Actions.AddRange(actions);
}
- public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
+ public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
{
Position = currentFrame.Position;
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
new file mode 100644
index 0000000000..254e220996
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public class DefaultFollowCircle : FollowCircle
+ {
+ public DefaultFollowCircle()
+ {
+ InternalChild = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 5,
+ BorderColour = Color4.Orange,
+ Blending = BlendingParameters.Additive,
+ Child = new Box
+ {
+ Colour = Color4.Orange,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.2f,
+ }
+ };
+ }
+
+ protected override void OnTrackingChanged(ValueChangedEvent tracking)
+ {
+ const float scale_duration = 300f;
+ const float fade_duration = 300f;
+
+ this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint)
+ .FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint);
+ }
+
+ protected override void OnSliderEnd()
+ {
+ const float fade_duration = 450f;
+
+ // intentionally pile on an extra FadeOut to make it happen much faster
+ this.FadeOut(fade_duration / 4, Easing.Out);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
new file mode 100644
index 0000000000..97bb4a3697
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
@@ -0,0 +1,111 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public class DefaultSliderBall : CompositeDrawable
+ {
+ private Box box = null!;
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? parentObject { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
+
+ InternalChild = new CircularContainer
+ {
+ Masking = true,
+ RelativeSizeAxes = Axes.Both,
+ Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ BorderThickness = 10,
+ BorderColour = Color4.White,
+ Alpha = 1,
+ Child = box = new Box
+ {
+ Blending = BlendingParameters.Additive,
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ AlwaysPresent = true,
+ Alpha = 0
+ }
+ };
+
+ if (parentObject != null)
+ {
+ var slider = (DrawableSlider)parentObject;
+ slider.Tracking.BindValueChanged(trackingChanged, true);
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (parentObject != null)
+ {
+ parentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(parentObject, parentObject.State.Value);
+ }
+ }
+
+ private void trackingChanged(ValueChangedEvent tracking) =>
+ box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ const float fade_duration = 450f;
+
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
+ {
+ this.FadeIn()
+ .ScaleTo(1f);
+ }
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
+ // intentionally pile on an extra FadeOut to make it happen much faster
+ this.FadeOut(fade_duration / 4, Easing.Out);
+
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(1.4f, fade_duration, Easing.Out);
+ break;
+ }
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (parentObject != null)
+ parentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+}
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/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs
new file mode 100644
index 0000000000..321705d25e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs
@@ -0,0 +1,75 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public abstract class FollowCircle : CompositeDrawable
+ {
+ [Resolved]
+ protected DrawableHitObject? ParentObject { get; private set; }
+
+ protected FollowCircle()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(OnTrackingChanged, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (ParentObject != null)
+ {
+ ParentObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(ParentObject);
+
+ ParentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(ParentObject, ParentObject.State.Value);
+ }
+ }
+
+ private void onHitObjectApplied(DrawableHitObject drawableObject)
+ {
+ this.ScaleTo(1f)
+ .FadeOut();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ OnSliderEnd();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (ParentObject != null)
+ {
+ ParentObject.HitObjectApplied -= onHitObjectApplied;
+ ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+
+ protected abstract void OnTrackingChanged(ValueChangedEvent tracking);
+
+ protected abstract void OnSliderEnd();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
new file mode 100644
index 0000000000..5b7da5a1ba
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+{
+ public class LegacyFollowCircle : FollowCircle
+ {
+ public LegacyFollowCircle(Drawable animationContent)
+ {
+ // follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
+ animationContent.Scale *= 0.5f;
+ animationContent.Anchor = Anchor.Centre;
+ animationContent.Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+ InternalChild = animationContent;
+ }
+
+ protected override void OnTrackingChanged(ValueChangedEvent tracking)
+ {
+ Debug.Assert(ParentObject != null);
+
+ if (ParentObject.Judged)
+ return;
+
+ double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current);
+
+ // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
+ // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
+ if (tracking.NewValue)
+ {
+ // TODO: Follow circle should bounce on each slider tick.
+ this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
+ .FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
+ }
+ else
+ {
+ // TODO: Should animate only at the next slider tick if we want to match stable perfectly.
+ this.ScaleTo(4f, 100)
+ .FadeTo(0f, 100);
+ }
+ }
+
+ protected override void OnSliderEnd()
+ {
+ this.ScaleTo(1.6f, 200, Easing.Out)
+ .FadeOut(200, Easing.In);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
index 07e60c82d0..414879f42d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
@@ -1,12 +1,12 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -18,8 +18,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly ISkin skin;
- private Sprite layerNd;
- private Sprite layerSpec;
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? parentObject { get; set; }
+
+ private Sprite layerNd = null!;
+ private Sprite layerSpec = null!;
public LegacySliderBall(Drawable animationContent, ISkin skin)
{
@@ -58,6 +61,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (parentObject != null)
+ {
+ parentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(parentObject, parentObject.State.Value);
+ }
+ }
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@@ -68,5 +82,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
layerNd.Rotation = -appliedRotation;
layerSpec.Rotation = -appliedRotation;
}
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which in this case have no visual impact (due to
+ // instant fade) but may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
+ this.FadeIn();
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ this.FadeOut();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (parentObject != null)
+ parentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index e7d28a9bd7..885a2c12fb 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -41,11 +41,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return this.GetAnimation(component.LookupName, false, false);
case OsuSkinComponents.SliderFollowCircle:
- var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
- if (followCircle != null)
- // follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
- followCircle.Scale *= 0.5f;
- return followCircle;
+ var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true);
+ if (followCircleContent != null)
+ return new LegacyFollowCircle(followCircleContent);
+
+ return null;
case OsuSkinComponents.SliderBall:
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
index 3a156d4d25..a9ae313a31 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -194,7 +194,28 @@ namespace osu.Game.Rulesets.Osu.Utils
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
{
var slider = (Slider)workingObject.HitObject;
- var possibleMovementBounds = calculatePossibleMovementBounds(slider);
+ 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;
@@ -239,10 +260,12 @@ namespace osu.Game.Rulesets.Osu.Utils
/// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
///
+ /// The for which to calculate a movement bounding box.
+ /// A which contains all of the possible movements of the slider such that the entire slider is inside the playfield.
///
/// If the slider is larger than the playfield, the returned may have negative width/height.
///
- private static RectangleF calculatePossibleMovementBounds(Slider slider)
+ public static RectangleF CalculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
@@ -353,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
{
///
@@ -395,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; }
@@ -405,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.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs
index 3553cb27dc..c86f8cb8d2 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs
@@ -24,22 +24,25 @@ namespace osu.Game.Rulesets.Taiko.Tests
new object[] { LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(TaikoModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } },
- new object[] { LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
- new object[] { LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) } },
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
- new object[] { LegacyMods.Cinema, new[] { typeof(TaikoModCinema) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }
};
+ [TestCaseSource(nameof(taiko_mod_mapping))]
+ [TestCase(LegacyMods.Cinema, new[] { typeof(TaikoModCinema) })]
+ [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })]
+ [TestCase(LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) })]
+ [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })]
+ [TestCase(LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) })]
+ [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })]
+ public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
+
[TestCaseSource(nameof(taiko_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })]
- public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
-
- [TestCaseSource(nameof(taiko_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new TaikoRuleset();
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index ce468d399b..51d4bbc630 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -1,7 +1,6 @@
-
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index 0d060988d6..72452e27b3 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -1,10 +1,9 @@
// 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 Newtonsoft.Json;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Taiko.Difficulty
@@ -54,9 +53,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
}
- public override void FromDatabaseAttributes(IReadOnlyDictionary values)
+ public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
{
- base.FromDatabaseAttributes(values);
+ base.FromDatabaseAttributes(values, onlineInfo);
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_DIFFICULTY];
diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
index f7f72df023..11136ad695 100644
--- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
+++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
@@ -1,10 +1,9 @@
// 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 osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Replays;
@@ -119,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
var nextHitObject = GetNextObject(i); // Get the next object that requires pressing the same button
bool canDelayKeyUp = nextHitObject == null || nextHitObject.StartTime > endTime + KEY_UP_DELAY;
- double calculatedDelay = canDelayKeyUp ? KEY_UP_DELAY : (nextHitObject.StartTime - endTime) * 0.9;
+ double calculatedDelay = canDelayKeyUp ? KEY_UP_DELAY : (nextHitObject.AsNonNull().StartTime - endTime) * 0.9;
Frames.Add(new TaikoReplayFrame(endTime + calculatedDelay));
hitButton = !hitButton;
diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs
index 0090409443..2f9b6c7f60 100644
--- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.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.Rulesets.Replays;
using System.Collections.Generic;
using System.Linq;
diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs
index d8f88785db..a0a687dca6 100644
--- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs
+++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.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 osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
@@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
Actions.AddRange(actions);
}
- public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
+ public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
{
if (currentFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim);
if (currentFrame.MouseRight2) Actions.Add(TaikoAction.RightRim);
diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
index c31aafa67f..9a8f29647d 100644
--- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
+++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Collections.IO
{
string firstRunName;
- using (var host = new CleanRunHeadlessGameHost(bypassCleanup: true))
+ using (var host = new CleanRunHeadlessGameHost(bypassCleanupOnDispose: true))
{
firstRunName = host.Name;
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
index be11476c73..9ee88c0670 100644
--- a/osu.Game.Tests/Database/BeatmapImporterTests.cs
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -35,7 +35,8 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using (var importer = new BeatmapImporter(storage, realm))
+ var importer = new BeatmapImporter(storage, realm);
+
using (new RealmRulesetStore(realm, storage))
{
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
@@ -76,7 +77,8 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using (var importer = new BeatmapImporter(storage, realm))
+ var importer = new BeatmapImporter(storage, realm);
+
using (new RealmRulesetStore(realm, storage))
{
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
@@ -134,7 +136,8 @@ namespace osu.Game.Tests.Database
var manager = new ModelManager(storage, realm);
- using (var importer = new BeatmapImporter(storage, realm))
+ var importer = new BeatmapImporter(storage, realm);
+
using (new RealmRulesetStore(realm, storage))
{
Task.Run(async () =>
@@ -160,7 +163,8 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using (var importer = new BeatmapImporter(storage, realm))
+ var importer = new BeatmapImporter(storage, realm);
+
using (new RealmRulesetStore(realm, storage))
{
var imported = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
@@ -187,7 +191,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
await LoadOszIntoStore(importer, realm.Realm);
@@ -199,7 +203,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -217,7 +221,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -231,7 +235,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? tempPath = TestResources.GetTestBeatmapForImport();
@@ -261,7 +265,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -281,7 +285,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -317,7 +321,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -366,7 +370,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -417,7 +421,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -465,7 +469,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -513,7 +517,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -548,7 +552,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var progressNotification = new ImportProgressNotification();
@@ -586,7 +590,7 @@ namespace osu.Game.Tests.Database
Interlocked.Increment(ref loggedExceptionCount);
};
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -607,6 +611,12 @@ namespace osu.Game.Tests.Database
using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
using (var zip = ZipArchive.Open(brokenOsz))
{
+ foreach (var entry in zip.Entries.ToArray())
+ {
+ if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture))
+ zip.RemoveEntry(entry);
+ }
+
zip.AddEntry("broken.osu", brokenOsu, false);
zip.SaveTo(outStream, CompressionType.Deflate);
}
@@ -627,7 +637,7 @@ namespace osu.Game.Tests.Database
checkSingleReferencedFileCount(realm.Realm, 18);
- Assert.AreEqual(1, loggedExceptionCount);
+ Assert.AreEqual(0, loggedExceptionCount);
File.Delete(brokenTempFilename);
});
@@ -638,7 +648,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm, batchImport: true);
@@ -665,7 +675,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
- using var importer = new BeatmapImporter(storage, realmFactory);
+ var importer = new BeatmapImporter(storage, realmFactory);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Realm);
@@ -697,7 +707,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -724,7 +734,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
@@ -750,7 +760,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var metadata = new BeatmapMetadata
@@ -798,7 +808,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -815,7 +825,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -851,7 +861,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -893,7 +903,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
@@ -944,7 +954,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealmAsync(async (realm, storage) =>
{
- using var importer = new BeatmapImporter(storage, realm);
+ var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index 65f805bafb..fd0b391d0d 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -2,11 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
+using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database
{
@@ -27,12 +30,91 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, _) =>
{
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
{
}
});
}
+ [Test]
+ public void TestAsyncWriteAsync()
+ {
+ RunTestWithRealmAsync(async (realm, _) =>
+ {
+ await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+
+ realm.Run(r => r.Refresh());
+
+ Assert.That(realm.Run(r => r.All().Count()), Is.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public void TestAsyncWriteWhileBlocking()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ Task writeTask;
+
+ using (realm.BlockAllOperations("testing"))
+ {
+ writeTask = realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+ Thread.Sleep(100);
+ Assert.That(writeTask.IsCompleted, Is.False);
+ }
+
+ writeTask.WaitSafely();
+
+ realm.Run(r => r.Refresh());
+ Assert.That(realm.Run(r => r.All().Count()), Is.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public void TestAsyncWrite()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
+
+ realm.Run(r => r.Refresh());
+
+ Assert.That(realm.Run(r => r.All().Count()), Is.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public void TestAsyncWriteAfterDisposal()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ realm.Dispose();
+ Assert.ThrowsAsync(() => realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())));
+ });
+ }
+
+ [Test]
+ public void TestAsyncWriteBeforeDisposal()
+ {
+ ManualResetEventSlim resetEvent = new ManualResetEventSlim();
+
+ RunTestWithRealm((realm, _) =>
+ {
+ var writeTask = realm.WriteAsync(r =>
+ {
+ // ensure that disposal blocks for our execution
+ Assert.That(resetEvent.Wait(100), Is.False);
+
+ r.Add(TestResources.CreateTestBeatmapSetInfo());
+ });
+
+ realm.Dispose();
+ resetEvent.Set();
+
+ writeTask.WaitSafely();
+ });
+ }
+
///
/// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks
/// due to context fetching semaphores.
@@ -87,7 +169,7 @@ namespace osu.Game.Tests.Database
Assert.Throws(() =>
{
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
{
}
});
@@ -95,7 +177,7 @@ namespace osu.Game.Tests.Database
stopThreadedUsage.Set();
// Ensure we can block a second time after the usage has ended.
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
{
}
});
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
index 00a667521d..3615cebe6a 100644
--- a/osu.Game.Tests/Database/RealmLiveTests.cs
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Database
{
migratedStorage.DeleteDirectory(string.Empty);
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
{
storage.Migrate(migratedStorage);
}
@@ -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()
{
@@ -91,6 +149,25 @@ namespace osu.Game.Tests.Database
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
}
+ [Test]
+ public void TestTransactionRolledBackOnException()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
+
+ realm.Run(r => r.Write(_ => r.Add(beatmap)));
+
+ var liveBeatmap = beatmap.ToLive(realm);
+
+ Assert.Throws(() => liveBeatmap.PerformWrite(l => throw new InvalidOperationException()));
+ Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
+
+ liveBeatmap.PerformWrite(l => l.Hidden = true);
+ Assert.IsTrue(liveBeatmap.PerformRead(l => l.Hidden));
+ });
+ }
+
[Test]
public void TestScopedReadWithoutContext()
{
diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
index b8ce036da1..4ee302bbd0 100644
--- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
+++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
-using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
@@ -84,11 +83,7 @@ namespace osu.Game.Tests.Database
realm.Run(r => r.Refresh());
- // Without forcing the write onto its own thread, realm will internally run the operation synchronously, which can cause a deadlock with `WaitSafely`.
- Task.Run(async () =>
- {
- await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
- }).WaitSafely();
+ realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
realm.Run(r => r.Refresh());
@@ -141,7 +136,7 @@ namespace osu.Game.Tests.Database
resolvedItems = null;
lastChanges = null;
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
Assert.That(resolvedItems, Is.Empty);
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
@@ -159,7 +154,7 @@ namespace osu.Game.Tests.Database
testEventsArriving(false);
// And make sure even after another context loss we don't get firings.
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
Assert.That(resolvedItems, Is.Null);
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
@@ -217,7 +212,7 @@ namespace osu.Game.Tests.Database
Assert.That(beatmapSetInfo, Is.Not.Null);
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
{
// custom disposal action fired when context lost.
Assert.That(beatmapSetInfo, Is.Null);
@@ -231,7 +226,7 @@ namespace osu.Game.Tests.Database
Assert.That(beatmapSetInfo, Is.Null);
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
Assert.That(beatmapSetInfo, Is.Null);
realm.Run(r => r.Refresh());
@@ -256,7 +251,7 @@ namespace osu.Game.Tests.Database
Assert.That(receivedValue, Is.Not.Null);
receivedValue = null;
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
{
}
@@ -267,7 +262,7 @@ namespace osu.Game.Tests.Database
subscription.Dispose();
receivedValue = null;
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("testing"))
Assert.That(receivedValue, Is.Null);
realm.Run(r => r.Refresh());
diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs
index ced397921c..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;
@@ -78,6 +77,16 @@ namespace osu.Game.Tests.Gameplay
}
[Test]
+ [FlakyTest]
+ /*
+ * Fail rate around 0.15%
+ *
+ * TearDown : osu.Framework.Testing.Drawables.Steps.AssertButton+TracedException : gameplay clock time = 2500
+ * --TearDown
+ * at osu.Framework.Threading.ScheduledDelegate.RunTaskInternal()
+ * at osu.Framework.Threading.Scheduler.Update()
+ * at osu.Framework.Graphics.Drawable.UpdateSubTree()
+ */
public void TestSeekPerformsInGameplayTime(
[Values(1.0, 0.5, 2.0)] double clockRate,
[Values(0.0, 200.0, -200.0)] double userOffset,
@@ -106,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/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
index de23b012c1..c887105da6 100644
--- a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
+++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
@@ -4,9 +4,12 @@
#nullable disable
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
+using osu.Game.Models;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.NonVisual
{
@@ -23,6 +26,47 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
}
+ [Test]
+ public void TestAudioEqualityNoFile()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ [Test]
+ public void TestAudioEqualitySameHash()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ addAudioFile(beatmapSetA, "abc");
+ addAudioFile(beatmapSetB, "abc");
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ [Test]
+ public void TestAudioEqualityDifferentHash()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ addAudioFile(beatmapSetA);
+ addAudioFile(beatmapSetB);
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ private static void addAudioFile(BeatmapSetInfo beatmapSetInfo, string hash = null)
+ {
+ beatmapSetInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = hash ?? Guid.NewGuid().ToString() }, "audio.mp3"));
+ }
+
[Test]
public void TestDatabasedWithDatabased()
{
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index a803974d30..216db2121c 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -44,8 +44,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestCustomDirectory()
{
- string customPath = prepareCustomPath();
-
+ using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost())
{
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
@@ -63,7 +62,6 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
- cleanupPath(customPath);
}
}
}
@@ -71,8 +69,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestSubDirectoryLookup()
{
- string customPath = prepareCustomPath();
-
+ using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost())
{
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
@@ -97,7 +94,6 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
- cleanupPath(customPath);
}
}
}
@@ -105,8 +101,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigration()
{
- string customPath = prepareCustomPath();
-
+ using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost())
{
try
@@ -173,7 +168,6 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
- cleanupPath(customPath);
}
}
}
@@ -181,9 +175,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationBetweenTwoTargets()
{
- string customPath = prepareCustomPath();
- string customPath2 = prepareCustomPath();
-
+ using (prepareCustomPath(out string customPath))
+ using (prepareCustomPath(out string customPath2))
using (var host = new CustomTestHeadlessGameHost())
{
try
@@ -205,8 +198,6 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
- cleanupPath(customPath);
- cleanupPath(customPath2);
}
}
}
@@ -214,8 +205,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToSameTargetFails()
{
- string customPath = prepareCustomPath();
-
+ using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost())
{
try
@@ -228,7 +218,6 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
- cleanupPath(customPath);
}
}
}
@@ -236,9 +225,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationFailsOnExistingData()
{
- string customPath = prepareCustomPath();
- string customPath2 = prepareCustomPath();
-
+ using (prepareCustomPath(out string customPath))
+ using (prepareCustomPath(out string customPath2))
using (var host = new CustomTestHeadlessGameHost())
{
try
@@ -267,8 +255,6 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
- cleanupPath(customPath);
- cleanupPath(customPath2);
}
}
}
@@ -276,8 +262,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToNestedTargetFails()
{
- string customPath = prepareCustomPath();
-
+ using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost())
{
try
@@ -298,7 +283,6 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
- cleanupPath(customPath);
}
}
}
@@ -306,8 +290,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToSeeminglyNestedTarget()
{
- string customPath = prepareCustomPath();
-
+ using (prepareCustomPath(out string customPath))
using (var host = new CustomTestHeadlessGameHost())
{
try
@@ -328,7 +311,26 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
- cleanupPath(customPath);
+ }
+ }
+ }
+
+ [Test]
+ public void TestBackupCreatedOnCorruptRealm()
+ {
+ using (var host = new CustomTestHeadlessGameHost())
+ {
+ try
+ {
+ File.WriteAllText(host.InitialStorage.GetFullPath(OsuGameBase.CLIENT_DATABASE_FILENAME, true), "i am definitely not a realm file");
+
+ LoadOsuIntoHost(host);
+
+ Assert.That(host.InitialStorage.GetFiles(string.Empty, "*_corrupt.realm"), Has.One.Items);
+ }
+ finally
+ {
+ host.Exit();
}
}
}
@@ -343,14 +345,17 @@ namespace osu.Game.Tests.NonVisual
return path;
}
- private static string prepareCustomPath() => Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}");
+ private static IDisposable prepareCustomPath(out string path)
+ {
+ path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}");
+ return new InvokeOnDisposal(path, cleanupPath);
+ }
private static void cleanupPath(string path)
{
try
{
- if (Directory.Exists(path))
- Directory.Delete(path, true);
+ if (Directory.Exists(path)) Directory.Delete(path, true);
}
catch
{
@@ -362,7 +367,7 @@ namespace osu.Game.Tests.NonVisual
public Storage InitialStorage { get; }
public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
- : base(callingMethodName: callingMethodName)
+ : base(callingMethodName: callingMethodName, bypassCleanupOnSetup: true)
{
string defaultStorageLocation = getDefaultLocationFor(this);
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
index 2657468b03..33204d33a7 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index 500f3159e2..bd0617515b 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.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 NUnit.Framework;
using osu.Game.Beatmaps;
@@ -256,7 +254,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
private class CustomFilterCriteria : IRulesetFilterCriteria
{
- public string CustomValue { get; set; }
+ public string? CustomValue { get; set; }
public bool Matches(BeatmapInfo beatmapInfo) => true;
diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
index a779fae510..14da07bc2d 100644
--- a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
+++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
@@ -83,14 +83,14 @@ namespace osu.Game.Tests.NonVisual
public override event Action NewResult
{
- add => throw new InvalidOperationException();
- remove => throw new InvalidOperationException();
+ add => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context");
+ remove => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context");
}
public override event Action RevertResult
{
- add => throw new InvalidOperationException();
- remove => throw new InvalidOperationException();
+ add => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
+ remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
}
public override Playfield Playfield { get; }
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index 9c8b977ad9..d1c5e2d8b3 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
var user = new APIUser { Id = 33 };
AddRepeatStep("add user multiple times", () => MultiplayerClient.AddUser(user), 3);
- AddAssert("room has 2 users", () => MultiplayerClient.Room?.Users.Count == 2);
+ AddUntilStep("room has 2 users", () => MultiplayerClient.ClientRoom?.Users.Count == 2);
}
[Test]
@@ -33,10 +33,10 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
var user = new APIUser { Id = 44 };
AddStep("add user", () => MultiplayerClient.AddUser(user));
- AddAssert("room has 2 users", () => MultiplayerClient.Room?.Users.Count == 2);
+ AddUntilStep("room has 2 users", () => MultiplayerClient.ClientRoom?.Users.Count == 2);
- AddRepeatStep("remove user multiple times", () => MultiplayerClient.RemoveUser(user), 3);
- AddAssert("room has 1 user", () => MultiplayerClient.Room?.Users.Count == 1);
+ AddStep("remove user", () => MultiplayerClient.RemoveUser(user));
+ AddUntilStep("room has 1 user", () => MultiplayerClient.ClientRoom?.Users.Count == 1);
}
[Test]
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
changeState(6, MultiplayerUserState.WaitingForLoad);
checkPlayingUserCount(6);
- AddStep("another user left", () => MultiplayerClient.RemoveUser((MultiplayerClient.Room?.Users.Last().User).AsNonNull()));
+ AddStep("another user left", () => MultiplayerClient.RemoveUser((MultiplayerClient.ServerRoom?.Users.Last().User).AsNonNull()));
checkPlayingUserCount(5);
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
@@ -103,7 +103,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
{
for (int i = 0; i < userCount; ++i)
{
- int userId = MultiplayerClient.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!");
+ int userId = MultiplayerClient.ServerRoom?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!");
MultiplayerClient.ChangeUserState(userId, state);
}
});
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 1d33a895eb..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, BeatmapOnlineLookupQueue onlineLookupQueue)
+ protected override BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm)
{
- return new TestBeatmapImporter(this, storage, realm, onlineLookupQueue);
+ return new TestBeatmapImporter(this, storage, realm);
}
internal class TestBeatmapImporter : BeatmapImporter
{
private readonly TestBeatmapManager testBeatmapManager;
- public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapOnlineLookupQueue beatmapOnlineLookupQueue)
- : base(storage, databaseAccess, beatmapOnlineLookupQueue)
+ public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess)
+ : base(storage, databaseAccess)
{
this.testBeatmapManager = testBeatmapManager;
}
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 91d4eb70e8..41404b2636 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -134,6 +134,7 @@ namespace osu.Game.Tests.Resources
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
StarRating = diff,
Length = length,
+ BeatmapSet = beatmapSet,
BPM = bpm,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
Ruleset = rulesetInfo,
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/Utils/NamingUtilsTest.cs b/osu.Game.Tests/Utils/NamingUtilsTest.cs
index 2195933197..62e688db90 100644
--- a/osu.Game.Tests/Utils/NamingUtilsTest.cs
+++ b/osu.Game.Tests/Utils/NamingUtilsTest.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.Linq;
using NUnit.Framework;
using osu.Game.Utils;
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 a204fc5686..6ad6f0b299 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -1,13 +1,12 @@
// 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;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Screens;
@@ -36,12 +35,12 @@ namespace osu.Game.Tests.Visual.Editing
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
- protected override bool EditorComponentsReady => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true;
-
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()
{
@@ -52,19 +51,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);
@@ -80,12 +79,33 @@ 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]
+ [FlakyTest]
+ /*
+ * Fail rate around 1.2%.
+ *
+ * Failing with realm refetch occasionally being null.
+ * My only guess is that the WorkingBeatmap at SetupScreen is dummy instead of the true one.
+ * If it's something else, we have larger issues with realm, but I don't think that's the case.
+ *
+ * at osu.Framework.Logging.ThrowingTraceListener.Fail(String message1, String message2)
+ * at System.Diagnostics.TraceInternal.Fail(String message, String detailMessage)
+ * at System.Diagnostics.TraceInternal.TraceProvider.Fail(String message, String detailMessage)
+ * at System.Diagnostics.Debug.Fail(String message, String detailMessage)
+ * at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.b__0(Realm realm) ModelManager.cs:line 50
+ * at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) RealmExtensions.cs:line 14
+ * at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) ModelManager.cs:line 47
+ * at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) ModelManager.cs:line 37
+ * at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) ResourcesSection.cs:line 115
+ * at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.b__11_0() TestSceneEditorBeatmapCreation.cs:line 101
+ */
public void TestAddAudioTrack()
{
+ AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
+
AddAssert("switch track to real track", () =>
{
var setup = Editor.ChildrenOfType().First();
@@ -95,20 +115,26 @@ namespace osu.Game.Tests.Visual.Editing
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
- using (var zip = ZipArchive.Open(temp))
- zip.WriteToDirectory(extractedFolder);
+ try
+ {
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
- bool success = setup.ChildrenOfType().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")));
+ bool success = setup.ChildrenOfType().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")));
- File.Delete(temp);
- Directory.Delete(extractedFolder, true);
+ // ensure audio file is copied to beatmap as "audio.mp3" rather than original filename.
+ Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3");
- // ensure audio file is copied to beatmap as "audio.mp3" rather than original filename.
- Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3");
-
- return success;
+ return success;
+ }
+ finally
+ {
+ File.Delete(temp);
+ Directory.Delete(extractedFolder, true);
+ }
});
+ AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
}
@@ -138,7 +164,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
@@ -157,7 +183,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;
});
@@ -173,7 +199,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
@@ -224,7 +250,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
@@ -240,7 +266,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;
});
@@ -259,13 +285,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", () =>
@@ -301,7 +327,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)");
@@ -337,7 +363,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/TestSceneEditorNavigation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs
index 85b50a9b21..327d581e37 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs
@@ -6,17 +6,14 @@ using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
-using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
-using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
-using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
@@ -38,15 +35,18 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
- AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded);
- AddStep("test gameplay", () =>
+ AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
+ AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay());
+
+ AddUntilStep("wait for player", () =>
{
- var testGameplayButton = this.ChildrenOfType().Single();
- InputManager.MoveMouseTo(testGameplayButton);
- InputManager.Click(MouseButton.Left);
+ // notifications may fire at almost any inopportune time and cause annoying test failures.
+ // relentlessly attempt to dismiss any and all interfering overlays, which includes notifications.
+ // this is theoretically not foolproof, but it's the best that can be done here.
+ Game.CloseAllOverlays();
+ return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded;
});
- AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded);
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index e9bbf1e33d..d7e9cc1bc0 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -6,10 +6,13 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Select;
@@ -22,7 +25,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestCantExitWithoutSaving()
{
+ AddUntilStep("Wait for dialog overlay load", () => ((Drawable)Game.Dependencies.Get()).IsLoaded);
AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
+ AddAssert("Sample playback disabled", () => Editor.SamplePlaybackDisabled.Value);
AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
}
@@ -39,6 +44,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");
@@ -130,6 +137,54 @@ namespace osu.Game.Tests.Visual.Editing
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
}
+ [Test]
+ public void TestLengthAndStarRatingUpdated()
+ {
+ WorkingBeatmap working = null;
+ double lastStarRating = 0;
+ double lastLength = 0;
+
+ 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));
+ AddAssert("One hitobject placed", () => EditorBeatmap.HitObjects.Count == 1);
+
+ SaveEditor();
+ AddStep("Get working beatmap", () => working = Game.BeatmapManager.GetWorkingBeatmap(EditorBeatmap.BeatmapInfo, true));
+
+ AddAssert("Beatmap length is zero", () => working.BeatmapInfo.Length == 0);
+ checkDifficultyIncreased();
+
+ AddStep("Move forward", () => InputManager.Key(Key.Right));
+ AddStep("Place another hitcircle", () => InputManager.Click(MouseButton.Left));
+ AddAssert("Two hitobjects placed", () => EditorBeatmap.HitObjects.Count == 2);
+
+ SaveEditor();
+ AddStep("Get working beatmap", () => working = Game.BeatmapManager.GetWorkingBeatmap(EditorBeatmap.BeatmapInfo, true));
+
+ checkDifficultyIncreased();
+ checkLengthIncreased();
+
+ void checkLengthIncreased()
+ {
+ AddStep("Beatmap length increased", () =>
+ {
+ Assert.That(working.BeatmapInfo.Length, Is.GreaterThan(lastLength));
+ lastLength = working.BeatmapInfo.Length;
+ });
+ }
+
+ void checkDifficultyIncreased()
+ {
+ AddStep("Beatmap difficulty increased", () =>
+ {
+ Assert.That(working.BeatmapInfo.StarRating, Is.GreaterThan(lastStarRating));
+ lastStarRating = working.BeatmapInfo.StarRating;
+ });
+ }
+ }
+
[Test]
public void TestExitWithoutSaveFromExistingBeatmap()
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
index fd103ff70f..2cada1989e 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
@@ -14,6 +14,16 @@ namespace osu.Game.Tests.Visual.Editing
public override Drawable CreateTestComponent() => Empty();
[Test]
+ [FlakyTest]
+ /*
+ * Fail rate around 0.3%
+ *
+ * TearDown : osu.Framework.Testing.Drawables.Steps.AssertButton+TracedException : range halved
+ * --TearDown
+ * at osu.Framework.Threading.ScheduledDelegate.RunTaskInternal()
+ * at osu.Framework.Threading.Scheduler.Update()
+ * at osu.Framework.Graphics.Drawable.UpdateSubTree()
+ */
public void TestVisibleRangeUpdatesOnZoomChange()
{
double initialVisibleRange = 0;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 019bfe322e..47c8dc0f8d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -6,10 +6,10 @@
using System.ComponentModel;
using System.Linq;
using osu.Framework.Testing;
-using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break;
@@ -31,19 +31,20 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps()
{
+ // It doesn't matter which ruleset is used - this beatmap is only used for reference.
+ var beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
- seekToBreak(0);
+
+ seekTo(beatmap.Beatmap.Breaks[0].StartTime);
AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting);
AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1);
+
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
- seekToBreak(0);
- seekToBreak(1);
-
- AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
-
+ seekTo(beatmap.Beatmap.HitObjects[^1].GetEndTime());
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
@@ -58,12 +59,18 @@ namespace osu.Game.Tests.Visual.Gameplay
ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen;
}
- private void seekToBreak(int breakIndex)
+ private void seekTo(double time)
{
- AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime));
- AddUntilStep("wait for seek to complete", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= destBreak().StartTime);
+ AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
- BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex);
+ // Prevent test timeouts by seeking in 10 second increments.
+ for (double t = 0; t < time; t += 10000)
+ {
+ double expectedTime = t;
+ AddUntilStep($"current time >= {t}", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= expectedTime);
+ }
+
+ AddUntilStep("wait for seek to complete", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= time);
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
index 6aedc64370..13ceb05aff 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
@@ -61,6 +61,16 @@ namespace osu.Game.Tests.Visual.Gameplay
/// Tests whether can still pause after cancelling completion by reverting back to true.
///
[Test]
+ [FlakyTest]
+ /*
+ * Fail rate around 0.45%
+ *
+ * TearDown : System.TimeoutException : "completion set by processor" timed out
+ * --TearDown
+ * at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
+ * at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
+ * at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
+ */
public void TestCanPauseAfterCancellation()
{
complete();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index 70d7f6a28b..707f807e64 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -273,14 +273,14 @@ namespace osu.Game.Tests.Visual.Gameplay
public override event Action NewResult
{
- add => throw new InvalidOperationException();
- remove => throw new InvalidOperationException();
+ add => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context");
+ remove => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context");
}
public override event Action RevertResult
{
- add => throw new InvalidOperationException();
- remove => throw new InvalidOperationException();
+ add => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
+ remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
}
public override Playfield Playfield { get; }
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
new file mode 100644
index 0000000000..5ec9e88728
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
@@ -0,0 +1,122 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Platform;
+using osu.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Ranking;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestScenePlayerLocalScoreImport : PlayerTestScene
+ {
+ private BeatmapManager beatmaps = null!;
+ private RulesetStore rulesets = null!;
+
+ private BeatmapSetInfo? importedSet;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, AudioManager audio)
+ {
+ Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
+ Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
+ Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, Scheduler, API));
+ Dependencies.Cache(Realm);
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("import beatmap", () =>
+ {
+ beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
+ importedSet = beatmaps.GetAllUsableBeatmapSets().First();
+ });
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => beatmaps.GetWorkingBeatmap(importedSet?.Beatmaps.First()).Beatmap;
+
+ private Ruleset? customRuleset;
+
+ protected override Ruleset CreatePlayerRuleset() => customRuleset ?? new OsuRuleset();
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
+
+ protected override bool HasCustomSteps => true;
+
+ protected override bool AllowFail => false;
+
+ [Test]
+ public void TestLastPlayedUpdated()
+ {
+ DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
+
+ AddStep("set no custom ruleset", () => customRuleset = null);
+ AddAssert("last played is null", () => getLastPlayed() == null);
+
+ CreateTest();
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+ AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
+ }
+
+ [Test]
+ public void TestScoreStoredLocally()
+ {
+ AddStep("set no custom ruleset", () => customRuleset = null);
+
+ CreateTest();
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null));
+ }
+
+ [Test]
+ public void TestScoreStoredLocallyCustomRuleset()
+ {
+ Ruleset createCustomRuleset() => new CustomRuleset();
+
+ AddStep("import custom ruleset", () => Realm.Write(r => r.Add(createCustomRuleset().RulesetInfo)));
+ AddStep("set custom ruleset", () => customRuleset = createCustomRuleset());
+
+ CreateTest();
+
+ AddAssert("score has custom ruleset", () => Player.Score.ScoreInfo.Ruleset.Equals(customRuleset.AsNonNull().RulesetInfo));
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null));
+ }
+
+ private class CustomRuleset : OsuRuleset, ILegacyRuleset
+ {
+ public override string Description => "custom";
+ public override string ShortName => "custom";
+
+ int ILegacyRuleset.LegacyID => -1;
+
+ public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
index e0c8989389..96efca6b65 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -365,21 +365,9 @@ namespace osu.Game.Tests.Visual.Gameplay
ImportedScore = score;
- // It was discovered that Score members could sometimes be half-populated.
- // In particular, the RulesetID property could be set to 0 even on non-osu! maps.
- // We want to test that the state of that property is consistent in this test.
- // EF makes this impossible.
- //
- // First off, because of the EF navigational property-explicit foreign key field duality,
- // it can happen that - for example - the Ruleset navigational property is correctly initialised to mania,
- // but the RulesetID foreign key property is not initialised and remains 0.
- // EF silently bypasses this by prioritising the Ruleset navigational property over the RulesetID foreign key one.
- //
- // Additionally, adding an entity to an EF DbSet CAUSES SIDE EFFECTS with regard to the foreign key property.
- // In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context,
- // RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3.
- //
- // For the above reasons, actual importing is disabled in this test.
+ // Calling base.ImportScore is omitted as it will fail for the test method which uses a custom ruleset.
+ // This can be resolved by doing something similar to what TestScenePlayerLocalScoreImport is doing,
+ // but requires a bit of restructuring.
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index 10a6b196b0..c259d5f0a8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -142,6 +142,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
+ [Test]
+ public void TestLocallyAvailableWithoutReplay()
+ {
+ Live imported = null;
+
+ AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(false, false)));
+
+ AddStep("create button without replay", () =>
+ {
+ Child = downloadButton = new TestReplayDownloadButton(imported.Value)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
+ });
+
+ AddUntilStep("wait for load", () => downloadButton.IsLoaded);
+
+ AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
+ }
+
[Test]
public void TestScoreImportThenDelete()
{
@@ -189,11 +211,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
- private ScoreInfo getScoreInfo(bool replayAvailable)
+ private ScoreInfo getScoreInfo(bool replayAvailable, bool hasOnlineId = true)
{
return new APIScore
{
- OnlineID = online_score_id,
+ OnlineID = hasOnlineId ? online_score_id : 0,
RulesetID = 0,
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
HasReplay = replayAvailable,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index 1e517efef2..5fad661e9b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -167,11 +167,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("start failing sends", () =>
{
spectatorClient.ShouldFailSendingFrames = true;
- framesReceivedSoFar = replay.Frames.Count;
frameSendAttemptsSoFar = spectatorClient.FrameSendAttempts;
});
- AddUntilStep("wait for send attempts", () => spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 5);
+ AddUntilStep("wait for next send attempt", () =>
+ {
+ framesReceivedSoFar = replay.Frames.Count;
+ return spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 1;
+ });
+
+ AddUntilStep("wait for more send attempts", () => spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 10);
AddAssert("frames did not increase", () => framesReceivedSoFar == replay.Frames.Count);
AddStep("stop failing sends", () => spectatorClient.ShouldFailSendingFrames = false);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
index 079d459beb..f0e184d727 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void createPlayerTest()
{
- CreateTest(null);
+ CreateTest();
AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null);
waitUntilStoryboardSamplesPlay();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 002e35f742..e2b2ad85a3 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -52,17 +52,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestStoryboardSkipOutro()
{
- CreateTest(null);
+ AddStep("set storyboard duration to long", () => currentStoryboardDuration = 200000);
+ CreateTest();
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
- AddAssert("player is no longer current screen", () => !Player.IsCurrentScreen());
+ AddUntilStep("player is no longer current screen", () => !Player.IsCurrentScreen());
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardNoSkipOutro()
{
- CreateTest(null);
+ CreateTest();
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
@@ -70,7 +71,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestStoryboardExitDuringOutroStillExits()
{
- CreateTest(null);
+ CreateTest();
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null);
@@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestCase(true)]
public void TestStoryboardToggle(bool enabledAtBeginning)
{
- CreateTest(null);
+ CreateTest();
AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning));
AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning));
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
@@ -129,7 +130,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType().First();
- CreateTest(null);
+ CreateTest();
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
@@ -143,7 +144,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestPerformExitNoOutro()
{
- CreateTest(null);
+ CreateTest();
AddStep("disable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, false));
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause());
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/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
index 074a92f5b0..2b461cf6f6 100644
--- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCreatedWithCorrectMode()
{
- AddAssert("room created with correct mode", () => MultiplayerClient.APIRoom?.QueueMode.Value == Mode);
+ AddUntilStep("room created with correct mode", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == Mode);
}
protected void RunGameplay()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs
index 86da9dc33d..5947cabf7f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneAllPlayersQueueMode.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play;
@@ -29,19 +30,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFirstItemSelectedByDefault()
{
- AddAssert("first item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID);
+ AddUntilStep("first item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
}
[Test]
public void TestItemAddedToTheEndOfQueue()
{
addItem(() => OtherBeatmap);
- AddAssert("playlist has 2 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2);
+ AddUntilStep("playlist has 2 items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 2);
addItem(() => InitialBeatmap);
- AddAssert("playlist has 3 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 3);
+ AddUntilStep("playlist has 3 items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 3);
- AddAssert("first item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID);
+ AddUntilStep("first item still selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
}
[Test]
@@ -49,9 +50,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
RunGameplay();
- AddAssert("playlist has only one item", () => MultiplayerClient.APIRoom?.Playlist.Count == 1);
- AddAssert("playlist item is expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true);
- AddAssert("last item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID);
+ AddUntilStep("playlist has only one item", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 1);
+ AddUntilStep("playlist item is expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[0].Expired == true);
+ AddUntilStep("last item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
}
[Test]
@@ -62,13 +63,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
RunGameplay();
- AddAssert("first item expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true);
- AddAssert("next item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[1].ID);
+ AddUntilStep("first item expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[0].Expired == true);
+ AddUntilStep("next item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[1].ID);
RunGameplay();
- AddAssert("second item expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == true);
- AddAssert("next item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[2].ID);
+ AddUntilStep("second item expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[1].Expired == true);
+ AddUntilStep("next item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[2].ID);
}
[Test]
@@ -81,9 +82,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
RunGameplay();
AddStep("change queue mode", () => MultiplayerClient.ChangeSettings(queueMode: QueueMode.HostOnly));
- AddAssert("playlist has 3 items", () => MultiplayerClient.APIRoom?.Playlist.Count == 3);
- AddAssert("item 2 is not expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == false);
- AddAssert("current item is the other beatmap", () => MultiplayerClient.Room?.Settings.PlaylistItemId == 2);
+ AddUntilStep("playlist has 3 items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 3);
+ AddUntilStep("item 2 is not expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[1].Expired == false);
+ AddUntilStep("current item is the other beatmap", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == 2);
}
[Test]
@@ -139,6 +140,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null);
AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded);
+ AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType().Single().InProgress.Value);
if (ruleset != null)
AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
index 757dfff2b7..1797c82fb9 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Cursor;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -195,12 +196,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestDownloadButtonHiddenWhenBeatmapExists()
{
- var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
Live imported = null;
- Debug.Assert(beatmap.BeatmapSet != null);
+ AddStep("import beatmap", () =>
+ {
+ var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
- AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet));
+ Debug.Assert(beatmap.BeatmapSet != null);
+ imported = manager.Import(beatmap.BeatmapSet);
+ });
createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach()));
@@ -245,40 +249,35 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestExpiredItems()
{
- AddStep("create playlist", () =>
+ createPlaylist(p =>
{
- Child = playlist = new TestPlaylist
+ p.Items.Clear();
+ p.Items.AddRange(new[]
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300),
- Items =
+ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
- new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ ID = 0,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ Expired = true,
+ RequiredMods = new[]
{
- ID = 0,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- Expired = true,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
- },
- new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
+ }
+ },
+ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ {
+ ID = 1,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ RequiredMods = new[]
{
- ID = 1,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
}
}
- };
+ });
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
@@ -321,19 +320,44 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible);
+ private void createPlaylistWithBeatmaps(Func> beatmaps) => createPlaylist(p =>
+ {
+ int index = 0;
+
+ p.Items.Clear();
+
+ foreach (var b in beatmaps())
+ {
+ p.Items.Add(new PlaylistItem(b)
+ {
+ ID = index++,
+ OwnerID = 2,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ RequiredMods = new[]
+ {
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
+ }
+ });
+ }
+ });
+
private void createPlaylist(Action setupPlaylist = null)
{
AddStep("create playlist", () =>
{
- Child = playlist = new TestPlaylist
+ Child = new OsuContextMenuContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300)
+ RelativeSizeAxes = Axes.Both,
+ Child = playlist = new TestPlaylist
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500, 300)
+ }
};
- setupPlaylist?.Invoke(playlist);
-
for (int i = 0; i < 20; i++)
{
playlist.Items.Add(new PlaylistItem(i % 2 == 1
@@ -360,39 +384,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
- });
- AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
- }
-
- private void createPlaylistWithBeatmaps(Func> beatmaps)
- {
- AddStep("create playlist", () =>
- {
- Child = playlist = new TestPlaylist
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300)
- };
-
- int index = 0;
-
- foreach (var b in beatmaps())
- {
- playlist.Items.Add(new PlaylistItem(b)
- {
- ID = index++,
- OwnerID = 2,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
- });
- }
+ setupPlaylist?.Invoke(playlist);
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs
index fe584fe3da..800b523a9d 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFirstItemSelectedByDefault()
{
- AddAssert("first item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID);
+ AddUntilStep("first item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
}
[Test]
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
selectNewItem(() => InitialBeatmap);
- AddAssert("playlist item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID);
+ AddUntilStep("playlist item still selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
}
[Test]
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
selectNewItem(() => OtherBeatmap);
- AddAssert("playlist item still selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[0].ID);
+ AddUntilStep("playlist item still selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[0].ID);
}
[Test]
@@ -48,10 +48,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
RunGameplay();
- AddAssert("playlist contains two items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2);
- AddAssert("first playlist item expired", () => MultiplayerClient.APIRoom?.Playlist[0].Expired == true);
- AddAssert("second playlist item not expired", () => MultiplayerClient.APIRoom?.Playlist[1].Expired == false);
- AddAssert("second playlist item selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == MultiplayerClient.APIRoom?.Playlist[1].ID);
+ AddUntilStep("playlist contains two items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 2);
+ AddUntilStep("first playlist item expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[0].Expired == true);
+ AddUntilStep("second playlist item not expired", () => MultiplayerClient.ClientAPIRoom?.Playlist[1].Expired == false);
+ AddUntilStep("second playlist item selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == MultiplayerClient.ClientAPIRoom?.Playlist[1].ID);
}
[Test]
@@ -60,12 +60,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
RunGameplay();
IBeatmapInfo firstBeatmap = null;
- AddStep("get first playlist item beatmap", () => firstBeatmap = MultiplayerClient.APIRoom?.Playlist[0].Beatmap);
+ AddStep("get first playlist item beatmap", () => firstBeatmap = MultiplayerClient.ServerAPIRoom?.Playlist[0].Beatmap);
selectNewItem(() => OtherBeatmap);
- AddAssert("first playlist item hasn't changed", () => MultiplayerClient.APIRoom?.Playlist[0].Beatmap == firstBeatmap);
- AddAssert("second playlist item changed", () => MultiplayerClient.APIRoom?.Playlist[1].Beatmap != firstBeatmap);
+ AddUntilStep("first playlist item hasn't changed", () => MultiplayerClient.ServerAPIRoom?.Playlist[0].Beatmap == firstBeatmap);
+ AddUntilStep("second playlist item changed", () => MultiplayerClient.ClientAPIRoom?.Playlist[1].Beatmap != firstBeatmap);
}
[Test]
@@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
QueueMode = QueueMode.AllPlayers
}).WaitSafely());
- AddUntilStep("api room updated", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+ AddUntilStep("api room updated", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
}
[Test]
@@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
addItem(() => OtherBeatmap);
- AddAssert("playlist contains two items", () => MultiplayerClient.APIRoom?.Playlist.Count == 2);
+ AddUntilStep("playlist contains two items", () => MultiplayerClient.ClientAPIRoom?.Playlist.Count == 2);
}
private void selectNewItem(Func beatmap)
@@ -104,6 +104,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
BeatmapInfo otherBeatmap = null;
+ AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType().Single().InProgress.Value);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
@@ -119,6 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
+ AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType().Single().InProgress.Value);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 171f8eea52..82e7bf8969 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -157,6 +157,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
+ [Test]
+ public void TestAccessTypeFiltering()
+ {
+ AddStep("add rooms", () =>
+ {
+ RoomManager.AddRooms(1, withPassword: true);
+ RoomManager.AddRooms(1, withPassword: false);
+ });
+
+ AddStep("apply default filter", () => container.Filter.SetDefault());
+
+ AddUntilStep("both rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
+
+ AddStep("filter public rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Public });
+
+ AddUntilStep("private room hidden", () => container.Rooms.All(r => !r.Room.HasPassword.Value));
+
+ AddStep("filter private rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Private });
+
+ AddUntilStep("public room hidden", () => container.Rooms.All(r => r.Room.HasPassword.Value));
+ }
+
[Test]
public void TestPasswordProtectedRooms()
{
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 d464527976..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)
@@ -116,25 +114,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
// all ready
AddUntilStep("all players ready", () =>
{
- var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
+ var nextUnready = multiplayerClient.ClientRoom?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null)
multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
- return multiplayerClient.Room?.Users.All(u => u.State == MultiplayerUserState.Ready) == true;
+ return multiplayerClient.ClientRoom?.Users.All(u => u.State == MultiplayerUserState.Ready) == true;
});
AddStep("unready all players at once", () =>
{
- Debug.Assert(multiplayerClient.Room != null);
+ Debug.Assert(multiplayerClient.ServerRoom != null);
- foreach (var u in multiplayerClient.Room.Users) multiplayerClient.ChangeUserState(u.UserID, MultiplayerUserState.Idle);
+ foreach (var u in multiplayerClient.ServerRoom.Users) multiplayerClient.ChangeUserState(u.UserID, MultiplayerUserState.Idle);
});
AddStep("ready all players at once", () =>
{
- Debug.Assert(multiplayerClient.Room != null);
+ Debug.Assert(multiplayerClient.ServerRoom != null);
- foreach (var u in multiplayerClient.Room.Users) multiplayerClient.ChangeUserState(u.UserID, MultiplayerUserState.Ready);
+ foreach (var u in multiplayerClient.ServerRoom.Users) multiplayerClient.ChangeUserState(u.UserID, MultiplayerUserState.Ready);
});
}
@@ -146,7 +144,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void removeLastUser()
{
- APIUser lastUser = multiplayerClient.Room?.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.Room?.Users.Last().User;
+ APIUser? lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
return;
@@ -166,14 +164,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void markNextPlayerReady()
{
- var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
+ var nextUnready = multiplayerClient.ServerRoom?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null)
multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}
private void markNextPlayerIdle()
{
- var nextUnready = multiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Ready);
+ var nextUnready = multiplayerClient.ServerRoom?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Ready);
if (nextUnready != null)
multiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Idle);
}
@@ -243,8 +241,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddAssert("Check participant count correct", () => multiplayerClient.APIRoom?.ParticipantCount.Value == 1);
- AddAssert("Check participant list contains user", () => multiplayerClient.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
+ AddUntilStep("Check participant count correct", () => multiplayerClient.ClientAPIRoom?.ParticipantCount.Value == 1);
+ AddUntilStep("Check participant list contains user", () => multiplayerClient.ClientAPIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
}
[Test]
@@ -303,8 +301,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => multiplayerClient.RoomJoined);
- AddAssert("Check participant count correct", () => multiplayerClient.APIRoom?.ParticipantCount.Value == 1);
- AddAssert("Check participant list contains user", () => multiplayerClient.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
+ AddUntilStep("Check participant count correct", () => multiplayerClient.ClientAPIRoom?.ParticipantCount.Value == 1);
+ AddUntilStep("Check participant list contains user", () => multiplayerClient.ClientAPIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
}
[Test]
@@ -323,7 +321,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddAssert("room has password", () => multiplayerClient.APIRoom?.Password.Value == "password");
+ AddUntilStep("room has password", () => multiplayerClient.ClientAPIRoom?.Password.Value == "password");
}
[Test]
@@ -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());
@@ -377,7 +375,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("change password", () => multiplayerClient.ChangeSettings(password: "password2"));
- AddUntilStep("local password changed", () => multiplayerClient.APIRoom?.Password.Value == "password2");
+ AddUntilStep("local password changed", () => multiplayerClient.ClientAPIRoom?.Password.Value == "password2");
}
[Test]
@@ -421,22 +419,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
- AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.Room?.Playlist.First().BeatmapID);
+ AddUntilStep("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.ClientRoom?.Playlist.First().BeatmapID);
AddStep("Select next beatmap", () => InputManager.Key(Key.Down));
- AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID);
+ AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.ClientRoom?.Playlist.First().BeatmapID);
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
- AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.Room?.Playlist.First().BeatmapID);
+ AddUntilStep("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == multiplayerClient.ClientRoom?.Playlist.First().BeatmapID);
}
[Test]
@@ -459,22 +457,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
- AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.Room?.Playlist.First().RulesetID);
+ AddUntilStep("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.ClientRoom?.Playlist.First().RulesetID);
AddStep("Switch ruleset", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Ruleset.Value = new CatchRuleset().RulesetInfo);
- AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID);
+ AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.ClientRoom?.Playlist.First().RulesetID);
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
- AddAssert("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.Room?.Playlist.First().RulesetID);
+ AddUntilStep("Ruleset matches current item", () => Ruleset.Value.OnlineID == multiplayerClient.ClientRoom?.Playlist.First().RulesetID);
}
[Test]
@@ -497,25 +495,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.Room?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
- AddAssert("Mods match current item",
- () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
+ AddUntilStep("Mods match current item",
+ () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.ClientRoom.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() });
- AddAssert("Mods don't match current item",
- () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
+ AddUntilStep("Mods don't match current item",
+ () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.ClientRoom.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
- AddAssert("Mods match current item",
- () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
+ AddUntilStep("Mods match current item",
+ () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.ClientRoom.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
}
[Test]
@@ -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]
@@ -890,7 +888,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
})).WaitSafely());
- AddUntilStep("item arrived in playlist", () => multiplayerClient.Room?.Playlist.Count == 2);
+ AddUntilStep("item arrived in playlist", () => multiplayerClient.ClientRoom?.Playlist.Count == 2);
AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
AddUntilStep("queue contains item", () => this.ChildrenOfType().Single().Items.Single().ID == 2);
@@ -921,10 +919,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
})).WaitSafely());
- AddUntilStep("item arrived in playlist", () => multiplayerClient.Room?.Playlist.Count == 2);
+ AddUntilStep("item arrived in playlist", () => multiplayerClient.ClientRoom?.Playlist.Count == 2);
AddStep("delete item as other user", () => multiplayerClient.RemoveUserPlaylistItem(1234, 2).WaitSafely());
- AddUntilStep("item removed from playlist", () => multiplayerClient.Room?.Playlist.Count == 1);
+ AddUntilStep("item removed from playlist", () => multiplayerClient.ClientRoom?.Playlist.Count == 1);
AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
AddUntilStep("queue is empty", () => this.ChildrenOfType().Single().Items.Count == 0);
@@ -957,7 +955,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
runGameplay();
AddStep("exit gameplay for other user", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Idle));
- AddUntilStep("wait for room to be idle", () => multiplayerClient.Room?.State == MultiplayerRoomState.Open);
+ AddUntilStep("wait for room to be idle", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.Open);
runGameplay();
@@ -969,9 +967,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
multiplayerClient.StartMatch().WaitSafely();
});
- AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
+ AddUntilStep("wait for loading", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.WaitingForLoad);
AddStep("set player loaded", () => multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Loaded));
- AddUntilStep("wait for gameplay to start", () => multiplayerClient.Room?.State == MultiplayerRoomState.Playing);
+ AddUntilStep("wait for gameplay to start", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.Playing);
AddUntilStep("wait for local user to enter spectator", () => multiplayerComponents.CurrentScreen is MultiSpectatorScreen);
}
}
@@ -992,11 +990,11 @@ 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", () =>
{
- user = playingUserId == null ? multiplayerClient.LocalUser : multiplayerClient.Room?.Users.Single(u => u.UserID == playingUserId);
+ user = playingUserId == null ? multiplayerClient.LocalUser : multiplayerClient.ServerRoom?.Users.Single(u => u.UserID == playingUserId);
lastState = user?.State ?? MultiplayerUserState.Idle;
InputManager.MoveMouseTo(readyButton);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 285258d7c0..ab4f9c37b2 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -107,7 +107,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
AddStep("select beatmap",
() => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID)));
+
AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
+ AddUntilStep("wait for ongoing operation to complete", () => !OnlinePlayDependencies.OngoingOperationTracker.InProgress.Value);
+
AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() });
AddStep("confirm selection", () => songSelect.FinaliseSelection());
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index bb7587ac56..5d6a6c8104 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -74,6 +74,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
+ [FlakyTest]
+ /*
+ * Fail rate around 1.5%
+ *
+ * TearDown : System.AggregateException : One or more errors occurred. (Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index'))
+ ----> System.ArgumentOutOfRangeException : Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
+ * --TearDown
+ * at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
+ * at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
+ * at osu.Framework.Extensions.TaskExtensions.WaitSafely(Task task)
+ * at osu.Framework.Testing.TestScene.checkForErrors()
+ * at osu.Framework.Testing.TestScene.RunTestsFromNUnit()
+ *--ArgumentOutOfRangeException
+ * at osu.Framework.Bindables.BindableList`1.removeAt(Int32 index, BindableList`1 caller)
+ * at osu.Framework.Bindables.BindableList`1.removeAt(Int32 index, BindableList`1 caller)
+ * at osu.Framework.Bindables.BindableList`1.removeAt(Int32 index, BindableList`1 caller)
+ * at osu.Game.Online.Multiplayer.MultiplayerClient.<>c__DisplayClass106_0.b__0() in C:\BuildAgent\work\ecd860037212ac52\osu.Game\Online\Multiplayer\MultiplayerClient .cs:line 702
+ * at osu.Framework.Threading.ScheduledDelegate.RunTaskInternal()
+ */
public void TestCreatedRoom()
{
AddStep("add playlist item", () =>
@@ -90,6 +109,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
+ [FlakyTest] // See above
public void TestTaikoOnlyMod()
{
AddStep("add playlist item", () =>
@@ -110,6 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
+ [FlakyTest] // See above
public void TestSettingValidity()
{
AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value);
@@ -126,6 +147,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
+ [FlakyTest] // See above
public void TestStartMatchWhileSpectating()
{
AddStep("set playlist", () =>
@@ -152,10 +174,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
ClickButtonWhenEnabled();
- AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
+ AddUntilStep("match started", () => MultiplayerClient.ClientRoom?.State == MultiplayerRoomState.WaitingForLoad);
}
[Test]
+ [FlakyTest] // See above
public void TestFreeModSelectionHasAllowedMods()
{
AddStep("add playlist item with allowed mod", () =>
@@ -176,13 +199,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("mod select contents loaded",
() => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded));
AddUntilStep("mod select contains only double time mod",
- () => this.ChildrenOfType()
- .SingleOrDefault()?
+ () => this.ChildrenOfType().Single().UserModsSelectOverlay
.ChildrenOfType()
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
}
[Test]
+ [FlakyTest] // See above
public void TestModSelectKeyWithAllowedMods()
{
AddStep("add playlist item with allowed mod", () =>
@@ -200,10 +223,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press toggle mod select key", () => InputManager.Key(Key.F1));
- AddUntilStep("mod select shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible);
+ AddUntilStep("mod select shown", () => this.ChildrenOfType().Single().UserModsSelectOverlay.State.Value == Visibility.Visible);
}
[Test]
+ [FlakyTest] // See above
public void TestModSelectKeyWithNoAllowedMods()
{
AddStep("add playlist item with no allowed mods", () =>
@@ -220,10 +244,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press toggle mod select key", () => InputManager.Key(Key.F1));
AddWaitStep("wait some", 3);
- AddAssert("mod select not shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
+ AddAssert("mod select not shown", () => this.ChildrenOfType().Single().UserModsSelectOverlay.State.Value == Visibility.Hidden);
}
[Test]
+ [FlakyTest] // See above
public void TestNextPlaylistItemSelectedAfterCompletion()
{
AddStep("add two playlist items", () =>
@@ -254,7 +279,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("last playlist item selected", () =>
{
- var lastItem = this.ChildrenOfType().Single(p => p.Item.ID == MultiplayerClient.APIRoom?.Playlist.Last().ID);
+ var lastItem = this.ChildrenOfType().Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID);
return lastItem.IsSelectedItem;
});
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index 75e3da05d3..edd1491865 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;
@@ -11,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
@@ -53,20 +52,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1);
AddStep("add non-resolvable user", () => MultiplayerClient.TestAddUnresolvedUser());
- AddAssert("null user added", () => MultiplayerClient.Room.AsNonNull().Users.Count(u => u.User == null) == 1);
+ AddUntilStep("null user added", () => MultiplayerClient.ClientRoom.AsNonNull().Users.Count(u => u.User == null) == 1);
AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2);
AddStep("kick null user", () => this.ChildrenOfType().Single(p => p.User.User == null)
.ChildrenOfType().Single().TriggerClick());
- AddAssert("null user kicked", () => MultiplayerClient.Room.AsNonNull().Users.Count == 1);
+ AddUntilStep("null user kicked", () => MultiplayerClient.ClientRoom.AsNonNull().Users.Count == 1);
}
[Test]
public void TestRemoveUser()
{
- APIUser secondUser = null;
+ APIUser? secondUser = null;
AddStep("add a user", () =>
{
@@ -80,7 +79,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.User == secondUser);
+ AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.UserID == secondUser?.Id);
}
[Test]
@@ -217,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("kick second user", () => this.ChildrenOfType().Single(d => d.IsPresent).TriggerClick());
- AddAssert("second user kicked", () => MultiplayerClient.Room?.Users.Single().UserID == API.LocalUser.Value.Id);
+ AddUntilStep("second user kicked", () => MultiplayerClient.ClientRoom?.Users.Single().UserID == API.LocalUser.Value.Id);
}
[Test]
@@ -368,17 +367,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewParticipantsList()
{
- ParticipantsList participantsList = null;
+ ParticipantsList? participantsList = null;
- AddStep("create new list", () => Child = participantsList = new ParticipantsList
+ AddStep("create new list", () => Child = new OsuContextMenuContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Y,
- Size = new Vector2(380, 0.7f)
+ RelativeSizeAxes = Axes.Both,
+ Child = participantsList = new ParticipantsList
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ 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/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
index 042a9297eb..f6a6b3c667 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs
@@ -30,10 +30,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("initialise gameplay", () =>
{
- Stack.Push(player = new MultiplayerPlayer(MultiplayerClient.APIRoom, new PlaylistItem(Beatmap.Value.BeatmapInfo)
+ Stack.Push(player = new MultiplayerPlayer(MultiplayerClient.ServerAPIRoom, new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
- }, MultiplayerClient.Room?.Users.ToArray()));
+ }, MultiplayerClient.ServerRoom?.Users.ToArray()));
});
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
index fcd6dd5bd2..e709a955b3 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300),
- Items = { BindTarget = MultiplayerClient.APIRoom!.Playlist }
+ Items = { BindTarget = MultiplayerClient.ClientAPIRoom!.Playlist }
};
});
@@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestDeleteButtonAlwaysVisibleForHost()
{
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
- AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+ AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
assertDeleteButtonVisibility(1, true);
@@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost()
{
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
- AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+ AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("set other user as host", () => MultiplayerClient.TransferHost(1234));
@@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestCurrentItemDoesNotHaveDeleteButton()
{
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
- AddUntilStep("wait for queue mode change", () => MultiplayerClient.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+ AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
@@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertDeleteButtonVisibility(1, true);
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
- AddUntilStep("wait for next item to be selected", () => MultiplayerClient.Room?.Settings.PlaylistItemId == 2);
+ AddUntilStep("wait for next item to be selected", () => MultiplayerClient.ClientRoom?.Settings.PlaylistItemId == 2);
AddUntilStep("wait for two items in playlist", () => playlist.ChildrenOfType().Count() == 2);
assertDeleteButtonVisibility(0, false);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
index 486da48449..91c87548c7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
@@ -99,10 +99,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestToggleWhenIdle(MultiplayerUserState initialState)
{
ClickButtonWhenEnabled();
- AddUntilStep("user is spectating", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Spectating);
+ AddUntilStep("user is spectating", () => MultiplayerClient.ClientRoom?.Users[0].State == MultiplayerUserState.Spectating);
ClickButtonWhenEnabled();
- AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
+ AddUntilStep("user is idle", () => MultiplayerClient.ClientRoom?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(MultiplayerRoomState.Closed)]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
index 2e39449f64..d80537a2e5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
@@ -76,8 +76,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddUntilStep("room type is team vs", () => multiplayerClient.Room?.Settings.MatchType == MatchType.TeamVersus);
- AddAssert("user state arrived", () => multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState);
+ AddUntilStep("room type is team vs", () => multiplayerClient.ClientRoom?.Settings.MatchType == MatchType.TeamVersus);
+ AddUntilStep("user state arrived", () => multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState);
}
[Test]
@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddAssert("user on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
+ AddUntilStep("user on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("add another user", () => multiplayerClient.AddUser(new APIUser { Username = "otheruser", Id = 44 }));
AddStep("press own button", () =>
@@ -104,17 +104,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().First());
InputManager.Click(MouseButton.Left);
});
- AddAssert("user on team 1", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
+ AddUntilStep("user on team 1", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
AddStep("press own button again", () => InputManager.Click(MouseButton.Left));
- AddAssert("user on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
+ AddUntilStep("user on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("press other user's button", () =>
{
InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
- AddAssert("user still on team 0", () => (multiplayerClient.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
+ AddUntilStep("user still on team 0", () => (multiplayerClient.ClientRoom?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
}
[Test]
@@ -133,14 +133,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddUntilStep("match type head to head", () => multiplayerClient.APIRoom?.Type.Value == MatchType.HeadToHead);
+ AddUntilStep("match type head to head", () => multiplayerClient.ClientAPIRoom?.Type.Value == MatchType.HeadToHead);
AddStep("change match type", () => multiplayerClient.ChangeSettings(new MultiplayerRoomSettings
{
MatchType = MatchType.TeamVersus
}).WaitSafely());
- AddUntilStep("api room updated to team versus", () => multiplayerClient.APIRoom?.Type.Value == MatchType.TeamVersus);
+ AddUntilStep("api room updated to team versus", () => multiplayerClient.ClientAPIRoom?.Type.Value == MatchType.TeamVersus);
}
[Test]
@@ -158,13 +158,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddUntilStep("room type is head to head", () => multiplayerClient.Room?.Settings.MatchType == MatchType.HeadToHead);
+ AddUntilStep("room type is head to head", () => multiplayerClient.ClientRoom?.Settings.MatchType == MatchType.HeadToHead);
AddUntilStep("team displays are not displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam == null));
AddStep("change to team vs", () => multiplayerClient.ChangeSettings(matchType: MatchType.TeamVersus));
- AddUntilStep("room type is team vs", () => multiplayerClient.Room?.Settings.MatchType == MatchType.TeamVersus);
+ AddUntilStep("room type is team vs", () => multiplayerClient.ClientRoom?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("team displays are displaying teams", () => multiplayerComponents.ChildrenOfType().All(d => d.DisplayedTeam != null));
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 1fb0195368..0b982a5745 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -39,8 +39,8 @@ namespace osu.Game.Tests.Visual.Online
private TestChatOverlay chatOverlay;
private ChannelManager channelManager;
- private APIUser testUser;
- private Channel testPMChannel;
+ private readonly APIUser testUser = new APIUser { Username = "test user", Id = 5071479 };
+
private Channel[] testChannels;
private Channel testChannel1 => testChannels[0];
@@ -52,8 +52,6 @@ namespace osu.Game.Tests.Visual.Online
[SetUp]
public void SetUp() => Schedule(() =>
{
- testUser = new APIUser { Username = "test user", Id = 5071479 };
- testPMChannel = new Channel(testUser);
testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
Child = new DependencyProvidingContainer
@@ -80,6 +78,14 @@ namespace osu.Game.Tests.Visual.Online
{
switch (req)
{
+ case CreateChannelRequest createRequest:
+ createRequest.TriggerSuccess(new APIChatChannel
+ {
+ ChannelID = ((int)createRequest.Channel.Id),
+ RecentMessages = new List()
+ });
+ return true;
+
case GetUpdatesRequest getUpdates:
getUpdates.TriggerFailure(new WebException());
return true;
@@ -181,7 +187,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Listing is visible", () => listingIsVisible);
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ joinTestChannel(0);
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
waitForChannel1Visible();
}
@@ -203,12 +209,11 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestChannelCloseButton()
{
+ var testPMChannel = new Channel(testUser);
+
AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join PM and public channels", () =>
- {
- channelManager.JoinChannel(testChannel1);
- channelManager.JoinChannel(testPMChannel);
- });
+ joinTestChannel(0);
+ joinChannel(testPMChannel);
AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddStep("Click close button", () =>
{
@@ -229,7 +234,7 @@ namespace osu.Game.Tests.Visual.Online
public void TestChatCommand()
{
AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ joinTestChannel(0);
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
AddAssert("PM channel is selected", () =>
@@ -248,14 +253,16 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestMultiplayerChannelIsNotShown()
{
- Channel multiplayerChannel = null;
+ Channel multiplayerChannel;
AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
+
+ joinChannel(multiplayerChannel = new Channel(new APIUser())
{
Name = "#mp_1",
Type = ChannelType.Multiplayer,
- }));
+ });
+
AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType()
.Where(item => item.IsPresent)
@@ -269,7 +276,7 @@ namespace osu.Game.Tests.Visual.Online
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ joinTestChannel(0);
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 1", () =>
{
@@ -291,8 +298,8 @@ namespace osu.Game.Tests.Visual.Online
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
+ joinTestChannel(0);
+ joinTestChannel(1);
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 2", () =>
{
@@ -314,8 +321,8 @@ namespace osu.Game.Tests.Visual.Online
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
- AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
+ joinTestChannel(0);
+ joinTestChannel(1);
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 2", () =>
{
@@ -337,7 +344,7 @@ namespace osu.Game.Tests.Visual.Online
{
Message message = null;
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ joinTestChannel(0);
AddStep("Send message in channel 1", () =>
{
testChannel1.AddNewMessages(message = new Message
@@ -357,7 +364,7 @@ namespace osu.Game.Tests.Visual.Online
{
Message message = null;
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ joinTestChannel(0);
AddStep("Send message in channel 1", () =>
{
testChannel1.AddNewMessages(message = new Message
@@ -378,7 +385,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ joinTestChannel(0);
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
waitForChannel1Visible();
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
@@ -404,11 +411,11 @@ namespace osu.Game.Tests.Visual.Online
chatOverlay.Show();
chatOverlay.SlowLoading = true;
});
- AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
+ joinTestChannel(0);
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddUntilStep("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading);
- AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
+ joinTestChannel(1);
AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2)));
AddUntilStep("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading);
@@ -461,19 +468,17 @@ namespace osu.Game.Tests.Visual.Online
Channel pmChannel1 = createPrivateChannel();
Channel pmChannel2 = createPrivateChannel();
- AddStep("Show overlay with channels", () =>
- {
- channelManager.JoinChannel(testChannel1);
- channelManager.JoinChannel(testChannel2);
- channelManager.JoinChannel(pmChannel1);
- channelManager.JoinChannel(pmChannel2);
- channelManager.JoinChannel(announceChannel);
- chatOverlay.Show();
- });
+ joinTestChannel(0);
+ joinTestChannel(1);
+ joinChannel(pmChannel1);
+ joinChannel(pmChannel2);
+ joinChannel(announceChannel);
+
+ AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
-
waitForChannel1Visible();
+
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
waitForChannel2Visible();
@@ -490,6 +495,18 @@ namespace osu.Game.Tests.Visual.Online
waitForChannel1Visible();
}
+ private void joinTestChannel(int i)
+ {
+ AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
+ AddUntilStep("wait for join completed", () => testChannels[i].Joined.Value);
+ }
+
+ private void joinChannel(Channel channel)
+ {
+ AddStep($"Join channel {channel}", () => channelManager.JoinChannel(channel));
+ AddUntilStep("wait for join completed", () => channel.Joined.Value);
+ }
+
private void waitForChannel1Visible() =>
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel?.Channel == testChannel1);
@@ -549,7 +566,7 @@ namespace osu.Game.Tests.Visual.Online
private Channel createPrivateChannel()
{
- int id = RNG.Next(0, 10000);
+ int id = RNG.Next(0, DummyAPIAccess.DUMMY_USER_ID - 1);
return new Channel(new APIUser
{
Id = id,
@@ -559,12 +576,13 @@ namespace osu.Game.Tests.Visual.Online
private Channel createAnnounceChannel()
{
- int id = RNG.Next(0, 10000);
+ const int announce_channel_id = 133337;
+
return new Channel
{
- Name = $"Announce {id}",
+ Name = $"Announce {announce_channel_id}",
Type = ChannelType.Announce,
- Id = id,
+ Id = announce_channel_id,
};
}
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.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
index 16a34e996f..be03328caa 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
@@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Users;
using osuTK.Graphics;
@@ -146,12 +147,12 @@ namespace osu.Game.Tests.Visual.Online
{
var scores = new APIScoresCollection
{
- Scores = new List
+ Scores = new List
{
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 6602580,
@@ -175,10 +176,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890,
Accuracy = 1,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 4608074,
@@ -201,10 +202,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789,
Accuracy = 0.9997,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 1014222,
@@ -226,10 +227,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678,
Accuracy = 0.9854,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 1541390,
@@ -250,10 +251,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567,
Accuracy = 0.8765,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 7151382,
@@ -275,12 +276,12 @@ namespace osu.Game.Tests.Visual.Online
foreach (var s in scores.Scores)
{
- s.Statistics = new Dictionary
+ s.Statistics = new Dictionary
{
- { "count_300", RNG.Next(2000) },
- { "count_100", RNG.Next(2000) },
- { "count_50", RNG.Next(2000) },
- { "count_miss", RNG.Next(2000) }
+ { HitResult.Great, RNG.Next(2000) },
+ { HitResult.Ok, RNG.Next(2000) },
+ { HitResult.Meh, RNG.Next(2000) },
+ { HitResult.Miss, RNG.Next(2000) }
};
}
@@ -289,10 +290,10 @@ namespace osu.Game.Tests.Visual.Online
private APIScoreWithPosition createUserBest() => new APIScoreWithPosition
{
- Score = new APIScore
+ Score = new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 7151382,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
index 77670c38f3..4510fda11d 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo
{
- Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException(),
+ Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException("osu!mania ruleset not found"),
Difficulty = new BeatmapDifficulty
{
CircleSize = 5,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index f9d18f4236..a144111fd3 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
@@ -53,13 +53,8 @@ namespace osu.Game.Tests.Visual.SongSelect
Margin = new MarginPadding { Top = 20 }
});
- AddStep("show", () =>
- {
- infoWedge.Show();
- infoWedge.Beatmap = Beatmap.Value;
- });
+ AddStep("show", () => infoWedge.Show());
- // select part is redundant, but wait for load isn't
selectBeatmap(Beatmap.Value.Beatmap);
AddWaitStep("wait for select", 3);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index 1b9b59676b..ef0c7d7d4d 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default));
- dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler));
+ dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
return dependencies;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 117515977e..504ded5406 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -166,15 +167,22 @@ namespace osu.Game.Tests.Visual.SongSelect
var beatmapSet = TestResources.CreateTestBeatmapSetInfo(rulesets.Length, rulesets);
- for (int i = 0; i < rulesets.Length; i++)
+ var importedBeatmapSet = Game.BeatmapManager.Import(beatmapSet);
+
+ Debug.Assert(importedBeatmapSet != null);
+
+ importedBeatmapSet.PerformWrite(s =>
{
- var beatmap = beatmapSet.Beatmaps[i];
+ for (int i = 0; i < rulesets.Length; i++)
+ {
+ var beatmap = s.Beatmaps[i];
- beatmap.StarRating = i + 1;
- beatmap.DifficultyName = $"SR{i + 1}";
- }
+ beatmap.StarRating = i + 1;
+ beatmap.DifficultyName = $"SR{i + 1}";
+ }
+ });
- return Game.BeatmapManager.Import(beatmapSet)?.Value;
+ return importedBeatmapSet.Value;
}
private bool ensureAllBeatmapSetsImported(IEnumerable beatmapSets) => beatmapSets.All(set => set != null);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneDifficultyRangeFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneDifficultyRangeFilterControl.cs
new file mode 100644
index 0000000000..7ae2c6e5e2
--- /dev/null
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneDifficultyRangeFilterControl.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 NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Game.Screens.Select;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.SongSelect
+{
+ public class TestSceneDifficultyRangeFilterControl : OsuTestScene
+ {
+ [Test]
+ public void TestBasic()
+ {
+ AddStep("create control", () =>
+ {
+ Child = new DifficultyRangeFilterControl
+ {
+ Width = 200,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(3),
+ };
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 6d881555da..159a3b1923 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -97,15 +97,32 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
}
+ [Test]
+ public void TestPlaceholderStarDifficulty()
+ {
+ addRulesetImportStep(0);
+ AddStep("change star filter", () => config.SetValue(OsuSetting.DisplayStarsMinimum, 10.0));
+
+ createSongSelect();
+
+ AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
+
+ AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick());
+
+ AddUntilStep("star filter reset", () => config.Get(OsuSetting.DisplayStarsMinimum) == 0.0);
+ AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
+ }
+
[Test]
public void TestPlaceholderConvertSetting()
{
- changeRuleset(2);
addRulesetImportStep(0);
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
createSongSelect();
+ changeRuleset(2);
+
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick());
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
index 8f890b2383..05b5c5c0cd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Dependencies.Cache(rulesets = new RealmRulesetStore(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(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 31406af87a..e59914f69a 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
@@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default));
- dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler));
+ dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
return dependencies;
@@ -100,6 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Rank = ScoreRank.XH,
User = new APIUser { Username = "TestUser" },
Ruleset = new OsuRuleset().RulesetInfo,
+ Files = { new RealmNamedFileUsage(new RealmFile { Hash = $"{i}" }, string.Empty) }
};
importedScores.Add(scoreManager.Import(score).Value);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
index 72e503dc33..181f46a996 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs
@@ -13,7 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
-using osu.Game.Overlays.Settings;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osuTK;
@@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep($"Set {name} slider to {value}", () =>
this.ChildrenOfType().First(c => c.LabelText == name)
- .ChildrenOfType>().First().Current.Value = value);
+ .ChildrenOfType>().First().Current.Value = value);
}
private void checkBindableAtValue(string name, float? expectedValue)
@@ -260,7 +260,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddAssert($"Slider {name} at {expectedValue}", () =>
this.ChildrenOfType().First(c => c.LabelText == name)
- .ChildrenOfType>().First().Current.Value == expectedValue);
+ .ChildrenOfType>().First().Current.Value == expectedValue);
}
private void setBeatmapWithDifficultyParameters(float value)
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs
index 0a0415789a..ce9aa682d1 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs
@@ -1,10 +1,10 @@
// 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.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
@@ -13,10 +13,24 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModIcon : OsuTestScene
{
+ [Test]
+ public void TestShowAllMods()
+ {
+ AddStep("create mod icons", () =>
+ {
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Full,
+ ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)),
+ };
+ });
+ }
+
[Test]
public void TestChangeModType()
{
- ModIcon icon = null;
+ ModIcon icon = null!;
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
AddStep("change mod", () => icon.Mod = new OsuModEasy());
@@ -25,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestInterfaceModType()
{
- ModIcon icon = null;
+ ModIcon icon = null!;
var ruleset = new OsuRuleset();
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 006707d064..4de70f6f9e 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
- AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8);
+ AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
index 4c35ec40b5..ee5ef2f364 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs
@@ -27,6 +27,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private Live first;
+ private const int item_count = 100;
+
[SetUp]
public void Setup() => Schedule(() =>
{
@@ -46,7 +48,7 @@ namespace osu.Game.Tests.Visual.UserInterface
beatmapSets.Clear();
- for (int i = 0; i < 100; i++)
+ for (int i = 0; i < item_count; i++)
{
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged());
}
@@ -59,6 +61,13 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestRearrangeItems()
{
+ AddUntilStep("wait for load complete", () =>
+ {
+ return this
+ .ChildrenOfType()
+ .Count(i => i.ChildrenOfType().First().DelayedLoadCompleted) > 6;
+ });
+
AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any());
AddStep("hold 1st item handle", () =>
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index a1eef4ce47..7615b3e8be 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -1,14 +1,13 @@
-
-
+
WinExe
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 6fd53d923b..5512b26863 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -4,7 +4,6 @@
osu.Game.Tournament.Tests.TournamentTestRunner
-
diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
index 0fc3646585..b088670caa 100644
--- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
+++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
@@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Components
{
public class TournamentSpriteTextWithBackground : CompositeDrawable
{
- protected readonly TournamentSpriteText Text;
+ public readonly TournamentSpriteText Text;
+
protected readonly Box Background;
public TournamentSpriteTextWithBackground(string text = "")
diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs
index c6bbb54f9a..2e79998e66 100644
--- a/osu.Game.Tournament/Components/TourneyVideo.cs
+++ b/osu.Game.Tournament/Components/TourneyVideo.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Components
private Video video;
private ManualClock manualClock;
+ public bool VideoAvailable => video != null;
+
public TourneyVideo(string filename, bool drawFallbackGradient = false)
{
this.filename = filename;
diff --git a/osu.Game.Tournament/Models/SeedingBeatmap.cs b/osu.Game.Tournament/Models/SeedingBeatmap.cs
index 03beb7ca9a..fb0e20556c 100644
--- a/osu.Game.Tournament/Models/SeedingBeatmap.cs
+++ b/osu.Game.Tournament/Models/SeedingBeatmap.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 Newtonsoft.Json;
using osu.Framework.Bindables;
@@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Models
public int ID;
[JsonProperty("BeatmapInfo")]
- public TournamentBeatmap Beatmap;
+ public TournamentBeatmap? Beatmap;
public long Score;
diff --git a/osu.Game.Tournament/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs
new file mode 100644
index 0000000000..b5e08fc005
--- /dev/null
+++ b/osu.Game.Tournament/SaveChangesOverlay.cs
@@ -0,0 +1,101 @@
+// 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.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Online.Multiplayer;
+using osuTK;
+
+namespace osu.Game.Tournament
+{
+ internal class SaveChangesOverlay : CompositeDrawable
+ {
+ [Resolved]
+ private TournamentGame tournamentGame { get; set; } = null!;
+
+ private string? lastSerialisedLadder;
+ private readonly TourneyButton saveChangesButton;
+
+ public SaveChangesOverlay()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = new Container
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Position = new Vector2(5),
+ CornerRadius = 10,
+ Masking = true,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ saveChangesButton = new TourneyButton
+ {
+ Text = "Save Changes",
+ Width = 140,
+ Height = 50,
+ Padding = new MarginPadding
+ {
+ Top = 10,
+ Left = 10,
+ },
+ Margin = new MarginPadding
+ {
+ Right = 10,
+ Bottom = 10,
+ },
+ Action = saveChanges,
+ // Enabled = { Value = false },
+ },
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ scheduleNextCheck();
+ }
+
+ private async Task checkForChanges()
+ {
+ string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder());
+
+ // If a save hasn't been triggered by the user yet, populate the initial value
+ lastSerialisedLadder ??= serialisedLadder;
+
+ if (lastSerialisedLadder != serialisedLadder && !saveChangesButton.Enabled.Value)
+ {
+ saveChangesButton.Enabled.Value = true;
+ saveChangesButton.Background
+ .FadeColour(saveChangesButton.BackgroundColour.Lighten(0.5f), 500, Easing.In).Then()
+ .FadeColour(saveChangesButton.BackgroundColour, 500, Easing.Out)
+ .Loop();
+ }
+
+ scheduleNextCheck();
+ }
+
+ private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
+
+ private void saveChanges()
+ {
+ tournamentGame.SaveChanges();
+ lastSerialisedLadder = tournamentGame.GetSerialisedLadder();
+
+ saveChangesButton.Enabled.Value = false;
+ saveChangesButton.Background.FadeColour(saveChangesButton.BackgroundColour, 500);
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
index 32da4d1b36..5ac25f97b5 100644
--- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
+++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
@@ -12,8 +12,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics;
@@ -45,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Drawings
public ITeamList TeamList;
[BackgroundDependencyLoader]
- private void load(TextureStore textures, Storage storage)
+ private void load(Storage storage)
{
RelativeSizeAxes = Axes.Both;
@@ -91,11 +89,10 @@ namespace osu.Game.Tournament.Screens.Drawings
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- new Sprite
+ new TourneyVideo("drawings")
{
+ Loop = true,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fill,
- Texture = textures.Get(@"Backgrounds/Drawings/background.png")
},
// Visualiser
new VisualiserContainer
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index 11db37c8b7..111893d18c 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -298,10 +298,10 @@ namespace osu.Game.Tournament.Screens.Editors
}, true);
}
- private void updatePanel()
+ private void updatePanel() => Scheduler.AddOnce(() =>
{
drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 };
- }
+ });
}
}
}
diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
index 8af5bbe513..0fefe6f780 100644
--- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
@@ -20,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Editors
{
- public abstract class TournamentEditorScreen : TournamentScreen, IProvideVideo
+ public abstract class TournamentEditorScreen : TournamentScreen
where TDrawable : Drawable, IModelBacked
where TModel : class, new()
{
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
index bb187c9e67..1eceddd871 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
@@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{
private readonly TeamScore score;
+ private readonly TournamentSpriteTextWithBackground teamText;
+
+ private readonly Bindable teamName = new Bindable("???");
+
private bool showScore;
public bool ShowScore
@@ -93,7 +97,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
}
}
},
- new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???")
+ teamText = new TournamentSpriteTextWithBackground
{
Scale = new Vector2(0.5f),
Origin = anchor,
@@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
updateDisplay();
FinishTransforms(true);
+
+ if (Team != null)
+ teamName.BindTo(Team.FullName);
+
+ teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
}
private void updateDisplay()
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
index ed11f097ed..5ee57e9271 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
@@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
currentMatch.BindTo(ladder.CurrentMatch);
currentMatch.BindValueChanged(matchChanged);
+ currentTeam.BindValueChanged(teamChanged);
+
updateMatch();
}
@@ -67,7 +69,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
// team may change to same team, which means score is not in a good state.
// thus we handle this manually.
- teamChanged(currentTeam.Value);
+ currentTeam.TriggerChange();
}
protected override bool OnMouseDown(MouseDownEvent e)
@@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
return base.OnMouseDown(e);
}
- private void teamChanged(TournamentTeam team)
+ private void teamChanged(ValueChangedEvent team)
{
InternalChildren = new Drawable[]
{
- teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
+ teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
};
}
}
diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
index 86b2c2a4e9..54ae4c0366 100644
--- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
@@ -21,7 +21,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Gameplay
{
- public class GameplayScreen : BeatmapInfoScreen, IProvideVideo
+ public class GameplayScreen : BeatmapInfoScreen
{
private readonly BindableBool warmup = new BindableBool();
diff --git a/osu.Game.Tournament/Screens/IProvideVideo.cs b/osu.Game.Tournament/Screens/IProvideVideo.cs
deleted file mode 100644
index aa67a5211f..0000000000
--- a/osu.Game.Tournament/Screens/IProvideVideo.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-namespace osu.Game.Tournament.Screens
-{
- ///
- /// Marker interface for a screen which provides its own local video background.
- ///
- public interface IProvideVideo
- {
- }
-}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
index f0eda5672a..1fdf616e34 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -53,6 +53,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection =>
{
+ // ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
+ GetContainingInputManager().TriggerFocusContention(null);
+
roundDropdown.Current = selection.NewValue?.Round;
losersCheckbox.Current = selection.NewValue?.Losers;
dateTimeBox.Current = selection.NewValue?.Date;
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
index 23bfa84afc..7ad7e76a1f 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Ladder
{
- public class LadderScreen : TournamentScreen, IProvideVideo
+ public class LadderScreen : TournamentScreen
{
protected Container MatchesContainer;
private Container paths;
diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
index 7a11e26794..0827cbae69 100644
--- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
+++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Schedule
{
- public class ScheduleScreen : TournamentScreen // IProvidesVideo
+ public class ScheduleScreen : TournamentScreen
{
private readonly Bindable currentMatch = new Bindable();
private Container mainContainer;
diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
index 42eff3565f..2b2dce3664 100644
--- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
+++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
@@ -9,6 +9,8 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Overlays;
@@ -19,7 +21,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Setup
{
- public class SetupScreen : TournamentScreen, IProvideVideo
+ public class SetupScreen : TournamentScreen
{
private FillFlowContainer fillFlow;
@@ -48,13 +50,21 @@ namespace osu.Game.Tournament.Screens.Setup
{
windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize);
- InternalChild = fillFlow = new FillFlowContainer
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Padding = new MarginPadding(10),
- Spacing = new Vector2(10),
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.2f),
+ },
+ fillFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(10),
+ Spacing = new Vector2(10),
+ }
};
api.LocalUser.BindValueChanged(_ => Schedule(reload));
@@ -74,7 +84,8 @@ namespace osu.Game.Tournament.Screens.Setup
Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()),
Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found",
Failing = fileBasedIpc?.IPCStorage == null,
- Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
+ Description =
+ "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
},
new ActionableInfo
{
diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
index 082aa99b0e..a7a175ceba 100644
--- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
+++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
@@ -14,7 +14,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Showcase
{
- public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo
+ public class ShowcaseScreen : BeatmapInfoScreen
{
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
index 925c697346..9262cab098 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -19,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro
{
- public class SeedingScreen : TournamentMatchScreen, IProvideVideo
+ public class SeedingScreen : TournamentMatchScreen
{
private Container mainContainer;
@@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
currentTeam.BindValueChanged(teamChanged, true);
}
- private void teamChanged(ValueChangedEvent team)
+ private void teamChanged(ValueChangedEvent team) => Scheduler.AddOnce(() =>
{
if (team.NewValue == null)
{
@@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
}
showTeam(team.NewValue);
- }
+ });
protected override void CurrentMatchChanged(ValueChangedEvent match)
{
@@ -120,8 +121,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro
foreach (var seeding in team.SeedingResults)
{
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
+
foreach (var beatmap in seeding.Beatmaps)
+ {
+ if (beatmap.Beatmap == null)
+ continue;
+
fill.Add(new BeatmapScoreRow(beatmap));
+ }
}
}
@@ -129,6 +136,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
{
public BeatmapScoreRow(SeedingBeatmap beatmap)
{
+ Debug.Assert(beatmap.Beatmap != null);
+
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Children = new Drawable[]
{
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
- new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
+ new TournamentSpriteText
+ { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
}
},
};
diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
index 98dfaa7487..08c9a7a897 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
@@ -13,7 +13,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro
{
- public class TeamIntroScreen : TournamentMatchScreen, IProvideVideo
+ public class TeamIntroScreen : TournamentMatchScreen
{
private Container mainContainer;
diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
index 50207547cd..ac54ff58f5 100644
--- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
@@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamWin
{
- public class TeamWinScreen : TournamentMatchScreen, IProvideVideo
+ public class TeamWinScreen : TournamentMatchScreen
{
private Container mainContainer;
@@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
private bool firstDisplay = true;
- private void update() => Schedule(() =>
+ private void update() => Scheduler.AddOnce(() =>
{
var match = CurrentMatch.Value;
diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index 537fbfc038..7d67bfa759 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -11,8 +11,6 @@ using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Logging;
using osu.Framework.Platform;
@@ -20,11 +18,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.Models;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tournament
{
+ [Cached]
public class TournamentGame : TournamentGameBase
{
public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE;
@@ -78,40 +76,9 @@ namespace osu.Game.Tournament
LoadComponentsAsync(new[]
{
- new Container
+ new SaveChangesOverlay
{
- CornerRadius = 10,
Depth = float.MinValue,
- Position = new Vector2(5),
- Masking = true,
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Children = new Drawable[]
- {
- new Box
- {
- Colour = OsuColour.Gray(0.2f),
- RelativeSizeAxes = Axes.Both,
- },
- new TourneyButton
- {
- Text = "Save Changes",
- Width = 140,
- Height = 50,
- Padding = new MarginPadding
- {
- Top = 10,
- Left = 10,
- },
- Margin = new MarginPadding
- {
- Right = 10,
- Bottom = 10,
- },
- Action = SaveChanges,
- },
- }
},
heightWarning = new WarningBox("Please make the window wider")
{
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 75c9f17d4c..f2a35ea5b3 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -295,7 +295,7 @@ namespace osu.Game.Tournament
}
}
- protected virtual void SaveChanges()
+ public void SaveChanges()
{
if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)
{
@@ -311,7 +311,16 @@ namespace osu.Game.Tournament
.ToList();
// Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state.
- string serialisedLadder = JsonConvert.SerializeObject(ladder,
+ string serialisedLadder = GetSerialisedLadder();
+
+ using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
+ using (var sw = new StreamWriter(stream))
+ sw.Write(serialisedLadder);
+ }
+
+ public string GetSerialisedLadder()
+ {
+ return JsonConvert.SerializeObject(ladder,
new JsonSerializerSettings
{
Formatting = Formatting.Indented,
@@ -319,10 +328,6 @@ namespace osu.Game.Tournament
DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() }
});
-
- using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
- using (var sw = new StreamWriter(stream))
- sw.Write(serialisedLadder);
}
protected override UserInputManager CreateUserInputManager() => new TournamentInputManager();
diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs
index 296b259d72..a12dbb4740 100644
--- a/osu.Game.Tournament/TournamentSceneManager.cs
+++ b/osu.Game.Tournament/TournamentSceneManager.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -186,7 +187,7 @@ namespace osu.Game.Tournament
var lastScreen = currentScreen;
currentScreen = target;
- if (currentScreen is IProvideVideo)
+ if (currentScreen.ChildrenOfType().FirstOrDefault()?.VideoAvailable == true)
{
video.FadeOut(200);
diff --git a/osu.Game.Tournament/TourneyButton.cs b/osu.Game.Tournament/TourneyButton.cs
index f5a82771f5..f1b14df783 100644
--- a/osu.Game.Tournament/TourneyButton.cs
+++ b/osu.Game.Tournament/TourneyButton.cs
@@ -3,12 +3,15 @@
#nullable disable
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tournament
{
public class TourneyButton : OsuButton
{
+ public new Box Background => base.Background;
+
public TourneyButton()
: base(null)
{
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index ef0fa36b16..bf7b980e75 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -81,6 +81,11 @@ namespace osu.Game.Beatmaps
}, true);
}
+ public void Invalidate(IBeatmapInfo beatmap)
+ {
+ base.Invalidate(lookup => lookup.BeatmapInfo.Equals(beatmap));
+ }
+
///
/// Retrieves a bindable containing the star difficulty of a that follows the currently-selected ruleset and mods.
///
@@ -207,7 +212,7 @@ namespace osu.Game.Beatmaps
if (cancellationToken.IsCancellationRequested)
return;
- var starDifficulty = task.GetResultSafely();
+ StarDifficulty? starDifficulty = task.GetResultSafely();
if (starDifficulty != null)
bindable.Value = starDifficulty.Value;
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index 237088036c..3e4d01a9a3 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -6,10 +6,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
-using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
@@ -18,10 +16,7 @@ using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.IO.Archives;
-using osu.Game.Models;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Skinning;
using Realms;
namespace osu.Game.Beatmaps
@@ -30,18 +25,17 @@ namespace osu.Game.Beatmaps
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
///
[ExcludeFromDynamicCompile]
- public class BeatmapImporter : RealmArchiveModelImporter, IDisposable
+ public class BeatmapImporter : RealmArchiveModelImporter
{
public override IEnumerable HandledExtensions => new[] { ".osz" };
protected override string[] HashableFileTypes => new[] { ".osu" };
- private readonly BeatmapOnlineLookupQueue? onlineLookupQueue;
+ public Action? ProcessBeatmap { private get; set; }
- public BeatmapImporter(Storage storage, RealmAccess realm, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
+ public BeatmapImporter(Storage storage, RealmAccess realm)
: base(storage, realm)
{
- this.onlineLookupQueue = onlineLookupQueue;
}
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
@@ -49,7 +43,7 @@ namespace osu.Game.Beatmaps
protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
{
if (archive != null)
- beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm));
+ beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm));
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
{
@@ -65,8 +59,7 @@ namespace osu.Game.Beatmaps
bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0);
- onlineLookupQueue?.Update(beatmapSet);
-
+ // TODO: this may no longer be valid as we aren't doing an online population at this point.
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0))
{
@@ -87,9 +80,8 @@ namespace osu.Game.Beatmaps
if (beatmapSet.OnlineID > 0)
{
- var existingSetWithSameOnlineID = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
-
- if (existingSetWithSameOnlineID != null)
+ // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure.
+ foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == beatmapSet.OnlineID))
{
existingSetWithSameOnlineID.DeletePending = true;
existingSetWithSameOnlineID.OnlineID = -1;
@@ -97,11 +89,18 @@ namespace osu.Game.Beatmaps
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineID = -1;
- LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted.");
+ LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion.");
}
}
}
+ protected override void PostImport(BeatmapSetInfo model, Realm realm)
+ {
+ base.PostImport(model, realm);
+
+ ProcessBeatmap?.Invoke(model);
+ }
+
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID > 0).Select(b => b.OnlineID).ToList();
@@ -200,23 +199,32 @@ namespace osu.Game.Beatmaps
///
/// Create all required s for the provided archive.
///
- private List createBeatmapDifficulties(IList files, Realm realm)
+ private List createBeatmapDifficulties(BeatmapSetInfo beatmapSet, Realm realm)
{
var beatmaps = new List();
- foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
+ foreach (var file in beatmapSet.Files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
{
using (var memoryStream = new MemoryStream(Files.Store.Get(file.File.GetStoragePath()))) // we need a memory stream so we can seek
{
IBeatmap decoded;
+
using (var lineReader = new LineBufferedReader(memoryStream, true))
+ {
+ if (lineReader.PeekLine() == null)
+ {
+ LogForModel(beatmapSet, $"No content found in beatmap file {file.Filename}.");
+ continue;
+ }
+
decoded = Decoder.GetDecoder(lineReader).Decode(lineReader);
+ }
string hash = memoryStream.ComputeSHA2Hash();
if (beatmaps.Any(b => b.Hash == hash))
{
- Logger.Log($"Skipping import of {file.Filename} due to duplicate file content.", LoggingTarget.Database);
+ LogForModel(beatmapSet, $"Skipping import of {file.Filename} due to duplicate file content.");
continue;
}
@@ -227,7 +235,7 @@ namespace osu.Game.Beatmaps
if (ruleset?.Available != true)
{
- Logger.Log($"Skipping import of {file.Filename} due to missing local ruleset {decodedInfo.Ruleset.OnlineID}.", LoggingTarget.Database);
+ LogForModel(beatmapSet, $"Skipping import of {file.Filename} due to missing local ruleset {decodedInfo.Ruleset.OnlineID}.");
continue;
}
@@ -278,64 +286,11 @@ namespace osu.Game.Beatmaps
MD5Hash = memoryStream.ComputeMD5Hash(),
};
- updateBeatmapStatistics(beatmap, decoded);
-
beatmaps.Add(beatmap);
}
}
return beatmaps;
}
-
- private void updateBeatmapStatistics(BeatmapInfo beatmap, IBeatmap decoded)
- {
- var rulesetInstance = ((IRulesetInfo)beatmap.Ruleset).CreateInstance();
-
- decoded.BeatmapInfo.Ruleset = rulesetInstance.RulesetInfo;
-
- // TODO: this should be done in a better place once we actually need to dynamically update it.
- beatmap.StarRating = rulesetInstance.CreateDifficultyCalculator(new DummyConversionBeatmap(decoded)).Calculate().StarRating;
- beatmap.Length = calculateLength(decoded);
- beatmap.BPM = 60000 / decoded.GetMostCommonBeatLength();
- }
-
- private double calculateLength(IBeatmap b)
- {
- if (!b.HitObjects.Any())
- return 0;
-
- var lastObject = b.HitObjects.Last();
-
- //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
- double endTime = lastObject.GetEndTime();
- double startTime = b.HitObjects.First().StartTime;
-
- return endTime - startTime;
- }
-
- public void Dispose()
- {
- onlineLookupQueue?.Dispose();
- }
-
- ///
- /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
- ///
- private class DummyConversionBeatmap : WorkingBeatmap
- {
- private readonly IBeatmap beatmap;
-
- public DummyConversionBeatmap(IBeatmap beatmap)
- : base(beatmap.BeatmapInfo, null)
- {
- this.beatmap = beatmap;
- }
-
- protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture? GetBackground() => null;
- protected override Track? GetBeatmapTrack() => null;
- protected internal override ISkin? GetSkin() => null;
- public override Stream? GetStream(string storagePath) => null;
- }
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 346bf86818..45d76259fc 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
@@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
public bool SamplesMatchPlaybackRate { get; set; } = true;
+ ///
+ /// The time at which this beatmap was last played by the local user.
+ ///
+ public DateTimeOffset? LastPlayed { get; set; }
+
///
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see ).
@@ -151,14 +157,23 @@ namespace osu.Game.Beatmaps
public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
- && BeatmapSet.Hash == other.BeatmapSet.Hash
- && Metadata.AudioFile == other.Metadata.AudioFile;
+ && compareFiles(this, other, m => m.AudioFile);
public bool BackgroundEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
- && BeatmapSet.Hash == other.BeatmapSet.Hash
- && Metadata.BackgroundFile == other.Metadata.BackgroundFile;
+ && compareFiles(this, other, m => m.BackgroundFile);
+
+ private static bool compareFiles(BeatmapInfo x, BeatmapInfo y, Func getFilename)
+ {
+ Debug.Assert(x.BeatmapSet != null);
+ Debug.Assert(y.BeatmapSet != null);
+
+ string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.BeatmapSet.Metadata))?.File.Hash;
+ string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.BeatmapSet.Metadata))?.File.Hash;
+
+ return fileHashX == fileHashY;
+ }
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 670dba14ec..30456afd2f 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -34,17 +34,18 @@ 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 BeatmapOnlineLookupQueue? onlineBeatmapLookupQueue;
+
+ 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, bool performOnlineLookups = false)
+ WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
: base(storage, realm)
{
if (performOnlineLookups)
@@ -52,14 +53,16 @@ namespace osu.Game.Beatmaps
if (api == null)
throw new ArgumentNullException(nameof(api), "API must be provided if online lookups are required.");
- onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
+ if (difficultyCache == null)
+ throw new ArgumentNullException(nameof(difficultyCache), "Difficulty cache must be provided if online lookups are required.");
}
var userResources = new RealmFileStore(realm, storage).Store;
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
- beatmapImporter = CreateBeatmapImporter(storage, realm, rulesets, onlineBeatmapLookupQueue);
+ beatmapImporter = CreateBeatmapImporter(storage, realm);
+ beatmapImporter.ProcessBeatmap = obj => ProcessBeatmap?.Invoke(obj);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
@@ -71,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, BeatmapOnlineLookupQueue? onlineLookupQueue) =>
- new BeatmapImporter(storage, realm, onlineLookupQueue);
+ protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm);
///
/// Create a new beatmap set, backed by a model,
@@ -314,10 +316,19 @@ namespace osu.Game.Beatmaps
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
- Realm.Write(r => setInfo.CopyChangesToRealm(r.Find(setInfo.ID)));
+ setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
+
+ Realm.Write(r =>
+ {
+ var liveBeatmapSet = r.Find(setInfo.ID);
+
+ setInfo.CopyChangesToRealm(liveBeatmapSet);
+
+ ProcessBeatmap?.Invoke(liveBeatmapSet);
+ });
}
- workingBeatmapCache.Invalidate(beatmapInfo);
+ Debug.Assert(beatmapInfo.BeatmapSet != null);
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
{
@@ -418,40 +429,44 @@ namespace osu.Game.Beatmaps
#region Implementation of IWorkingBeatmapCache
- public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? beatmapInfo)
+ ///
+ /// Retrieve a instance for the provided
+ ///
+ /// The beatmap to lookup.
+ /// Whether to force a refetch from the database to ensure is up-to-date.
+ /// A instance correlating to the provided .
+ public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? beatmapInfo, bool refetch = false)
{
- // Detached sets don't come with files.
- // If we seem to be missing files, now is a good time to re-fetch.
- if (beatmapInfo?.IsManaged == true || beatmapInfo?.BeatmapSet?.Files.Count == 0)
+ if (beatmapInfo != null)
{
- Realm.Run(r =>
+ // Detached sets don't come with files.
+ // If we seem to be missing files, now is a good time to re-fetch.
+ if (refetch || beatmapInfo.IsManaged || beatmapInfo.BeatmapSet?.Files.Count == 0)
{
- var refetch = r.Find(beatmapInfo.ID)?.Detach();
+ workingBeatmapCache.Invalidate(beatmapInfo);
- if (refetch != null)
- beatmapInfo = refetch;
- });
+ Guid id = beatmapInfo.ID;
+ beatmapInfo = Realm.Run(r => r.Find(id)?.Detach()) ?? beatmapInfo;
+ }
+
+ Debug.Assert(beatmapInfo.IsManaged != true);
}
- Debug.Assert(beatmapInfo?.IsManaged != true);
-
return workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
}
+ WorkingBeatmap IWorkingBeatmapCache.GetWorkingBeatmap(BeatmapInfo beatmapInfo) => GetWorkingBeatmap(beatmapInfo);
void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo);
void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo);
- public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID));
-
- #endregion
-
- #region Implementation of IDisposable
-
- public void Dispose()
+ public event Action? OnInvalidated
{
- onlineBeatmapLookupQueue?.Dispose();
+ add => workingBeatmapCache.OnInvalidated += value;
+ remove => workingBeatmapCache.OnInvalidated -= value;
}
+ public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID));
+
#endregion
#region Implementation of IPostImports
diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs
new file mode 100644
index 0000000000..20fa0bc7c6
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapUpdater.cs
@@ -0,0 +1,105 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Platform;
+using osu.Game.Database;
+using osu.Game.Online.API;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Beatmaps
+{
+ ///
+ /// Handles all processing required to ensure a local beatmap is in a consistent state with any changes.
+ ///
+ public class BeatmapUpdater : IDisposable
+ {
+ private readonly IWorkingBeatmapCache workingBeatmapCache;
+ private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
+ private readonly BeatmapDifficultyCache difficultyCache;
+
+ public BeatmapUpdater(IWorkingBeatmapCache workingBeatmapCache, BeatmapDifficultyCache difficultyCache, IAPIProvider api, Storage storage)
+ {
+ this.workingBeatmapCache = workingBeatmapCache;
+ this.difficultyCache = difficultyCache;
+
+ onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
+ }
+
+ ///
+ /// Queue a beatmap for background processing.
+ ///
+ public void Queue(int beatmapSetId)
+ {
+ // TODO: implement
+ }
+
+ ///
+ /// Queue a beatmap for background processing.
+ ///
+ public void Queue(Live beatmap)
+ {
+ // For now, just fire off a task.
+ // TODO: Add actual queueing probably.
+ Task.Factory.StartNew(() => beatmap.PerformRead(Process));
+ }
+
+ ///
+ /// Run all processing on a beatmap immediately.
+ ///
+ public void Process(BeatmapSetInfo beatmapSet) => beatmapSet.Realm.Write(r =>
+ {
+ // Before we use below, we want to invalidate.
+ workingBeatmapCache.Invalidate(beatmapSet);
+
+ onlineLookupQueue.Update(beatmapSet);
+
+ foreach (var beatmap in beatmapSet.Beatmaps)
+ {
+ difficultyCache.Invalidate(beatmap);
+
+ var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
+ var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
+
+ Debug.Assert(ruleset != null);
+
+ var calculator = ruleset.CreateDifficultyCalculator(working);
+
+ beatmap.StarRating = calculator.Calculate().StarRating;
+ beatmap.Length = calculateLength(working.Beatmap);
+ beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
+ }
+
+ // 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)
+ {
+ if (!b.HitObjects.Any())
+ return 0;
+
+ var lastObject = b.HitObjects.Last();
+
+ //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
+ double endTime = lastObject.GetEndTime();
+ double startTime = b.HitObjects.First().StartTime;
+
+ return endTime - startTime;
+ }
+
+ #region Implementation of IDisposable
+
+ public void Dispose()
+ {
+ if (onlineLookupQueue.IsNotNull())
+ onlineLookupQueue.Dispose();
+ }
+
+ #endregion
+ }
+}
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index a5e6ac0a1c..52e760a068 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Formats
{
Section section = Section.General;
- string line;
+ string? line;
while ((line = stream.ReadLine()) != null)
{
diff --git a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs
index 160b7cf0ae..e1634e7d24 100644
--- a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs
+++ b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs
@@ -13,25 +13,50 @@ namespace osu.Game.Beatmaps
///
int? MaxCombo { get; }
+ ///
+ /// The approach rate.
+ ///
+ float ApproachRate { get; }
+
+ ///
+ /// The circle size.
+ ///
+ float CircleSize { get; }
+
+ ///
+ /// The drain rate.
+ ///
+ float DrainRate { get; }
+
+ ///
+ /// The overall difficulty.
+ ///
+ float OverallDifficulty { get; }
+
///
/// The amount of circles in this beatmap.
///
- public int CircleCount { get; }
+ int CircleCount { get; }
///
/// The amount of sliders in this beatmap.
///
- public int SliderCount { get; }
+ int SliderCount { get; }
+
+ ///
+ /// The amount of spinners in tihs beatmap.
+ ///
+ int SpinnerCount { get; }
///
/// The amount of plays this beatmap has.
///
- public int PlayCount { get; }
+ int PlayCount { get; }
///
/// The amount of passes this beatmap has.
///
- public int PassCount { get; }
+ int PassCount { get; }
APIFailTimes? FailTimes { get; }
}
diff --git a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs
index ad9cac6957..3eb33f10d6 100644
--- a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/IWorkingBeatmapCache.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
-
namespace osu.Game.Beatmaps
{
public interface IWorkingBeatmapCache
diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs
index e042f1c698..6aac275a6a 100644
--- a/osu.Game/Beatmaps/StarDifficulty.cs
+++ b/osu.Game/Beatmaps/StarDifficulty.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps
///
public StarDifficulty([NotNull] DifficultyAttributes attributes)
{
- Stars = attributes.StarRating;
+ Stars = double.IsFinite(attributes.StarRating) ? attributes.StarRating : 0;
MaxCombo = attributes.MaxCombo;
Attributes = attributes;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
@@ -46,7 +46,7 @@ namespace osu.Game.Beatmaps
///
public StarDifficulty(double starDifficulty, int maxCombo)
{
- Stars = starDifficulty;
+ Stars = double.IsFinite(starDifficulty) ? starDifficulty : 0;
MaxCombo = maxCombo;
Attributes = null;
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index 4495fb8318..ce883a7092 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -76,10 +76,13 @@ namespace osu.Game.Beatmaps
{
Logger.Log($"Invalidating working beatmap cache for {info}");
workingCache.Remove(working);
+ OnInvalidated?.Invoke(working);
}
}
}
+ public event Action OnInvalidated;
+
public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
{
if (beatmapInfo?.BeatmapSet == null)
@@ -134,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)
{
@@ -151,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)
{
@@ -170,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)
{
@@ -189,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)
{
@@ -208,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/Collections/CollectionToggleMenuItem.cs b/osu.Game/Collections/CollectionToggleMenuItem.cs
new file mode 100644
index 0000000000..f2b10305b8
--- /dev/null
+++ b/osu.Game/Collections/CollectionToggleMenuItem.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Collections
+{
+ public class CollectionToggleMenuItem : ToggleMenuItem
+ {
+ public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap)
+ : base(collection.Name.Value, MenuItemType.Standard, state =>
+ {
+ if (state)
+ collection.BeatmapHashes.Add(beatmap.MD5Hash);
+ else
+ collection.BeatmapHashes.Remove(beatmap.MD5Hash);
+ })
+ {
+ State.Value = collection.BeatmapHashes.Contains(beatmap.MD5Hash);
+ }
+ }
+}
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/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs
index 896d111989..3b5424b3fb 100644
--- a/osu.Game/Database/EFToRealmMigrator.cs
+++ b/osu.Game/Database/EFToRealmMigrator.cs
@@ -126,17 +126,18 @@ namespace osu.Game.Database
string backupSuffix = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
// required for initial backup.
- var realmBlockOperations = realm.BlockAllOperations();
+ var realmBlockOperations = realm.BlockAllOperations("EF migration");
Task.Factory.StartNew(() =>
{
try
{
- realm.CreateBackup(Path.Combine(backup_folder, $"client.{backupSuffix}.realm"), realmBlockOperations);
+ realm.CreateBackup(Path.Combine(backup_folder, $"client.{backupSuffix}.realm"));
}
finally
{
- // Above call will dispose of the blocking token when done.
+ // Once the backup is created, we need to stop blocking operations so the migration can complete.
+ realmBlockOperations.Dispose();
// Clean up here so we don't accidentally dispose twice.
realmBlockOperations = null;
}
diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs
index 6e6d928dcc..571a9ccc7c 100644
--- a/osu.Game/Database/MemoryCachingComponent.cs
+++ b/osu.Game/Database/MemoryCachingComponent.cs
@@ -3,11 +3,14 @@
#nullable disable
+using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
+using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Statistics;
namespace osu.Game.Database
{
@@ -19,8 +22,16 @@ namespace osu.Game.Database
{
private readonly ConcurrentDictionary cache = new ConcurrentDictionary();
+ private readonly GlobalStatistic statistics;
+
protected virtual bool CacheNullValues => true;
+ protected MemoryCachingComponent()
+ {
+ statistics = GlobalStatistics.Get(nameof(MemoryCachingComponent), GetType().ReadableName());
+ statistics.Value = new MemoryCachingStatistics();
+ }
+
///
/// Retrieve the cached value for the given lookup.
///
@@ -29,16 +40,39 @@ namespace osu.Game.Database
protected async Task GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
{
if (CheckExists(lookup, out TValue performance))
+ {
+ statistics.Value.HitCount++;
return performance;
+ }
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
+ statistics.Value.MissCount++;
+
if (computed != null || CacheNullValues)
+ {
cache[lookup] = computed;
+ statistics.Value.Usage = cache.Count;
+ }
return computed;
}
+ ///
+ /// Invalidate all entries matching a provided predicate.
+ ///
+ /// The predicate to decide which keys should be invalidated.
+ protected void Invalidate(Func matchKeyPredicate)
+ {
+ foreach (var kvp in cache)
+ {
+ if (matchKeyPredicate(kvp.Key))
+ cache.TryRemove(kvp.Key, out _);
+ }
+
+ statistics.Value.Usage = cache.Count;
+ }
+
protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
cache.TryGetValue(lookup, out value);
@@ -49,5 +83,31 @@ namespace osu.Game.Database
/// An optional to cancel the operation.
/// The computed value.
protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default);
+
+ private class MemoryCachingStatistics
+ {
+ ///
+ /// Total number of cache hits.
+ ///
+ public int HitCount;
+
+ ///
+ /// Total number of cache misses.
+ ///
+ public int MissCount;
+
+ ///
+ /// Total number of cached entities.
+ ///
+ public int Usage;
+
+ public override string ToString()
+ {
+ int totalAccesses = HitCount + MissCount;
+ double hitRate = totalAccesses == 0 ? 0 : (double)HitCount / totalAccesses;
+
+ return $"i:{Usage} h:{HitCount} m:{MissCount} {hitRate:0%}";
+ }
+ }
}
}
diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs
index 9603412178..4224c92f2c 100644
--- a/osu.Game/Database/ModelManager.cs
+++ b/osu.Game/Database/ModelManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using osu.Framework.Platform;
@@ -46,6 +47,7 @@ namespace osu.Game.Database
Realm.Realm.Write(realm =>
{
var managed = realm.Find(item.ID);
+ Debug.Assert(managed != null);
operation(managed);
item.Files.Clear();
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 3ea7a14826..02b5a51f1f 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -58,15 +58,19 @@ namespace osu.Game.Database
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
+ /// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
///
- private const int schema_version = 14;
+ private const int schema_version = 15;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
///
private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1);
- private readonly ThreadLocal currentThreadCanCreateRealmInstances = new ThreadLocal();
+ ///
+ /// true when the current thread has already entered the .
+ ///
+ private readonly ThreadLocal currentThreadHasRealmRetrievalLock = new ThreadLocal();
///
/// Holds a map of functions registered via and and a coinciding action which when triggered,
@@ -98,10 +102,14 @@ namespace osu.Game.Database
private static readonly GlobalStatistic total_writes_async = GlobalStatistics.Get(@"Realm", @"Writes (Async)");
- private readonly object realmLock = new object();
-
private Realm? updateRealm;
+ ///
+ /// Tracks whether a realm was ever fetched from this instance.
+ /// After a fetch occurs, blocking operations will be guaranteed to restore any subscriptions.
+ ///
+ private bool hasInitialisedOnce;
+
private bool isSendingNotificationResetEvents;
public Realm Realm => ensureUpdateRealm();
@@ -116,23 +124,21 @@ namespace osu.Game.Database
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"Use {nameof(getRealmInstance)} when performing realm operations from a non-update thread");
- lock (realmLock)
+ if (updateRealm == null)
{
- if (updateRealm == null)
- {
- updateRealm = getRealmInstance();
+ updateRealm = getRealmInstance();
+ hasInitialisedOnce = true;
- Logger.Log(@$"Opened realm ""{updateRealm.Config.DatabasePath}"" at version {updateRealm.Config.SchemaVersion}");
+ 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;
+ // Resubscribe any subscriptions
+ foreach (var action in customSubscriptionsResetMap.Keys.ToArray())
+ registerSubscription(action);
}
+
+ Debug.Assert(updateRealm != null);
+
+ return updateRealm;
}
internal static bool CurrentThreadSubscriptionsAllowed => current_thread_subscriptions_allowed.Value;
@@ -182,14 +188,14 @@ namespace osu.Game.Database
// If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
if (!storage.Exists(newerVersionFilename))
- CreateBackup(newerVersionFilename);
+ createBackup(newerVersionFilename);
storage.Delete(Filename);
}
else
{
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
- CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
+ createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
storage.Delete(Filename);
}
@@ -234,7 +240,7 @@ namespace osu.Game.Database
}
// For extra safety, also store the temporarily-used database which we are about to replace.
- CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_newer_version_before_recovery{realm_extension}");
+ createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_newer_version_before_recovery{realm_extension}");
storage.Delete(Filename);
@@ -381,16 +387,29 @@ namespace osu.Game.Database
}
}
+ private readonly CountdownEvent pendingAsyncWrites = new CountdownEvent(0);
+
///
/// Write changes to realm asynchronously, guaranteeing order of execution.
///
/// The work to run.
public Task WriteAsync(Action action)
{
+ if (isDisposed)
+ throw new ObjectDisposedException(nameof(RealmAccess));
+
+ // Required to ensure the write is tracked and accounted for before disposal.
+ // Can potentially be avoided if we have a need to do so in the future.
+ if (!ThreadSafety.IsUpdateThread)
+ throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread.");
+
+ // CountdownEvent will fail if already at zero.
+ if (!pendingAsyncWrites.TryAddCount())
+ pendingAsyncWrites.Reset(1);
+
// Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval.
// Adding a forced Task.Run resolves this.
-
- return Task.Run(async () =>
+ var writeTask = Task.Run(async () =>
{
total_writes_async.Value++;
@@ -400,7 +419,11 @@ namespace osu.Game.Database
using (var realm = getRealmInstance())
// ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]).
await realm.WriteAsync(() => action(realm));
+
+ pendingAsyncWrites.Signal();
});
+
+ return writeTask;
}
///
@@ -425,14 +448,15 @@ namespace osu.Game.Database
public IDisposable RegisterForNotifications(Func> query, NotificationCallbackDelegate callback)
where T : RealmObjectBase
{
- lock (realmLock)
- {
- Func action = realm => query(realm).QueryAsyncWithNotifications(callback);
+ Func action = realm => query(realm).QueryAsyncWithNotifications(callback);
+ lock (notificationsResetMap)
+ {
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet(), null, null));
- return RegisterCustomSubscription(action);
}
+
+ return RegisterCustomSubscription(action);
}
///
@@ -523,15 +547,17 @@ namespace osu.Game.Database
void unsubscribe()
{
- lock (realmLock)
+ if (customSubscriptionsResetMap.TryGetValue(action, out var unsubscriptionAction))
{
- if (customSubscriptionsResetMap.TryGetValue(action, out var unsubscriptionAction))
+ unsubscriptionAction?.Dispose();
+ customSubscriptionsResetMap.Remove(action);
+
+ lock (notificationsResetMap)
{
- unsubscriptionAction?.Dispose();
- customSubscriptionsResetMap.Remove(action);
notificationsResetMap.Remove(action);
- total_subscriptions.Value--;
}
+
+ total_subscriptions.Value--;
}
}
});
@@ -541,19 +567,16 @@ namespace osu.Game.Database
{
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;
+ // 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);
+ 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;
- }
+ current_thread_subscriptions_allowed.Value = true;
+ customSubscriptionsResetMap[action] = action(realm);
+ current_thread_subscriptions_allowed.Value = false;
}
private Realm getRealmInstance()
@@ -565,10 +588,11 @@ namespace osu.Game.Database
try
{
- if (!currentThreadCanCreateRealmInstances.Value)
+ // Ensure that the thread that currently has the `realmRetrievalLock` can retrieve nested contexts and not deadlock on itself.
+ if (!currentThreadHasRealmRetrievalLock.Value)
{
realmRetrievalLock.Wait();
- currentThreadCanCreateRealmInstances.Value = true;
+ currentThreadHasRealmRetrievalLock.Value = true;
tookSemaphoreLock = true;
}
else
@@ -592,7 +616,7 @@ namespace osu.Game.Database
if (tookSemaphoreLock)
{
realmRetrievalLock.Release();
- currentThreadCanCreateRealmInstances.Value = false;
+ currentThreadHasRealmRetrievalLock.Value = false;
}
}
}
@@ -759,28 +783,37 @@ namespace osu.Game.Database
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
efContextFactory?.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
- public void CreateBackup(string backupFilename, IDisposable? blockAllOperations = null)
+ ///
+ /// Create a full realm backup.
+ ///
+ /// The filename for the backup.
+ public void CreateBackup(string backupFilename)
{
- using (blockAllOperations ?? BlockAllOperations())
+ if (realmRetrievalLock.CurrentCount != 0)
+ throw new InvalidOperationException($"Call {nameof(BlockAllOperations)} before creating a backup.");
+
+ createBackup(backupFilename);
+ }
+
+ private void createBackup(string backupFilename)
+ {
+ Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database);
+
+ int attempts = 10;
+
+ while (attempts-- > 0)
{
- Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database);
-
- int attempts = 10;
-
- while (attempts-- > 0)
+ try
{
- try
- {
- using (var source = storage.GetStream(Filename, mode: FileMode.Open))
- using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
- source.CopyTo(destination);
- return;
- }
- catch (IOException)
- {
- // file may be locked during use.
- Thread.Sleep(500);
- }
+ using (var source = storage.GetStream(Filename, mode: FileMode.Open))
+ using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
+ source.CopyTo(destination);
+ return;
+ }
+ catch (IOException)
+ {
+ // file may be locked during use.
+ Thread.Sleep(500);
}
}
}
@@ -792,9 +825,15 @@ namespace osu.Game.Database
/// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm.
/// ie. to move the realm backing file to a new location.
///
+ /// The reason for blocking. Used for logging purposes.
/// An which should be disposed to end the blocking section.
- public IDisposable BlockAllOperations()
+ public IDisposable BlockAllOperations(string reason)
{
+ Logger.Log($@"Attempting to block all realm operations for {reason}.", LoggingTarget.Database);
+
+ if (!ThreadSafety.IsUpdateThread)
+ throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
+
if (isDisposed)
throw new ObjectDisposedException(nameof(RealmAccess));
@@ -804,39 +843,27 @@ namespace osu.Game.Database
{
realmRetrievalLock.Wait();
- lock (realmLock)
+ if (hasInitialisedOnce)
{
- if (updateRealm == null)
+ syncContext = SynchronizationContext.Current;
+
+ // Before disposing the update context, clean up all subscriptions.
+ // Note that in the case of realm notification subscriptions, this is not really required (they will be cleaned up by disposal).
+ // In the case of custom subscriptions, we want them to fire before the update realm is disposed in case they do any follow-up work.
+ foreach (var action in customSubscriptionsResetMap.ToArray())
{
- // 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);
+ action.Value?.Dispose();
+ customSubscriptionsResetMap[action.Key] = null;
}
- else
- {
- if (!ThreadSafety.IsUpdateThread)
- throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
-
- syncContext = SynchronizationContext.Current;
-
- // Before disposing the update context, clean up all subscriptions.
- // Note that in the case of realm notification subscriptions, this is not really required (they will be cleaned up by disposal).
- // In the case of custom subscriptions, we want them to fire before the update realm is disposed in case they do any follow-up work.
- foreach (var action in customSubscriptionsResetMap.ToArray())
- {
- action.Value?.Dispose();
- customSubscriptionsResetMap[action.Key] = null;
- }
- }
-
- Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
updateRealm?.Dispose();
updateRealm = null;
}
+ Logger.Log(@"Lock acquired for blocking operations", LoggingTarget.Database);
+
const int sleep_length = 200;
- int timeout = 5000;
+ int timeSpent = 0;
try
{
@@ -844,10 +871,10 @@ namespace osu.Game.Database
while (!Compact())
{
Thread.Sleep(sleep_length);
- timeout -= sleep_length;
+ timeSpent += sleep_length;
- if (timeout < 0)
- throw new TimeoutException(@"Took too long to acquire lock");
+ if (timeSpent > 5000)
+ throw new TimeoutException($@"Realm compact failed after {timeSpent / sleep_length} attempts over {timeSpent / 1000} seconds");
}
}
catch (RealmException e)
@@ -857,6 +884,8 @@ namespace osu.Game.Database
Logger.Log($"Realm compact failed with error {e}", LoggingTarget.Database);
}
+ Logger.Log(@"Realm usage isolated via compact", LoggingTarget.Database);
+
// In order to ensure events arrive in the correct order, these *must* be fired post disposal of the update realm,
// and must be posted to the synchronization context.
// This is because realm may fire event callbacks between the `unregisterAllSubscriptions` and `updateRealm.Dispose`
@@ -870,8 +899,11 @@ namespace osu.Game.Database
try
{
- foreach (var action in notificationsResetMap.Values)
- action();
+ lock (notificationsResetMap)
+ {
+ foreach (var action in notificationsResetMap.Values)
+ action();
+ }
}
finally
{
@@ -889,16 +921,39 @@ namespace osu.Game.Database
void restoreOperation()
{
+ // Release of lock needs to happen here rather than on the update thread, as there may be another
+ // operation already blocking the update thread waiting for the blocking operation to complete.
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
realmRetrievalLock.Release();
+ if (syncContext == null) return;
+
+ ManualResetEventSlim updateRealmReestablished = new ManualResetEventSlim();
+
// Post back to the update thread to revive any subscriptions.
// In the case we are on the update thread, let's also require this to run synchronously.
// This requirement is mostly due to test coverage, but shouldn't cause any harm.
if (ThreadSafety.IsUpdateThread)
- syncContext?.Send(_ => ensureUpdateRealm(), null);
+ {
+ syncContext.Send(_ =>
+ {
+ ensureUpdateRealm();
+ updateRealmReestablished.Set();
+ }, null);
+ }
else
- syncContext?.Post(_ => ensureUpdateRealm(), null);
+ {
+ syncContext.Post(_ =>
+ {
+ ensureUpdateRealm();
+ updateRealmReestablished.Set();
+ }, null);
+ }
+
+ // Wait for the post to complete to ensure a second `Migrate` operation doesn't start in the mean time.
+ // This is important to ensure `ensureUpdateRealm` is run before another blocking migration operation starts.
+ if (!updateRealmReestablished.Wait(10000))
+ throw new TimeoutException(@"Reestablishing update realm after block took too long");
}
}
@@ -909,10 +964,10 @@ namespace osu.Game.Database
public void Dispose()
{
- lock (realmLock)
- {
- updateRealm?.Dispose();
- }
+ if (!pendingAsyncWrites.Wait(10000))
+ Logger.Log("Realm took too long waiting on pending async writes", level: LogLevel.Error);
+
+ updateRealm?.Dispose();
if (!isDisposed)
{
diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs
index a3349d9918..aa7fac07a8 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)
@@ -296,7 +294,8 @@ namespace osu.Game.Database
try
{
- LogForModel(item, @"Beginning import...");
+ // Log output here will be missing a valid hash in non-batch imports.
+ LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}...");
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
using (var transaction = realm.BeginWrite())
@@ -310,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)
{
@@ -335,11 +338,11 @@ namespace osu.Game.Database
// import to store
realm.Add(item);
+ PostImport(item, realm);
+
transaction.Commit();
}
- PostImport(item, realm);
-
LogForModel(item, @"Import successfully completed!");
}
catch (Exception e)
@@ -385,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();
@@ -476,7 +479,7 @@ namespace osu.Game.Database
}
///
- /// Perform any final actions after the import has been committed to the database.
+ /// Perform any final actions before the import has been committed to the database.
///
/// The model prepared for import.
/// The current realm context.
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/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs
index 72de747807..9c871a3929 100644
--- a/osu.Game/Database/RealmLive.cs
+++ b/osu.Game/Database/RealmLive.cs
@@ -104,9 +104,12 @@ namespace osu.Game.Database
PerformRead(t =>
{
- var transaction = t.Realm.BeginWrite();
- perform(t);
- transaction.Commit();
+ using (var transaction = t.Realm.BeginWrite())
+ {
+ perform(t);
+ transaction.Commit();
+ }
+
RealmLiveStatistics.WRITES.Value++;
});
}
diff --git a/osu.Game/Extensions/CollectionExtensions.cs b/osu.Game/Extensions/CollectionExtensions.cs
index c573d169f1..473dc4b8f4 100644
--- a/osu.Game/Extensions/CollectionExtensions.cs
+++ b/osu.Game/Extensions/CollectionExtensions.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;
namespace osu.Game.Extensions
diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs
index c2c00e342b..d1aba2bfe3 100644
--- a/osu.Game/Extensions/DrawableExtensions.cs
+++ b/osu.Game/Extensions/DrawableExtensions.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 Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
diff --git a/osu.Game/Extensions/LanguageExtensions.cs b/osu.Game/Extensions/LanguageExtensions.cs
index 3b1e9c7719..b67e7fb6fc 100644
--- a/osu.Game/Extensions/LanguageExtensions.cs
+++ b/osu.Game/Extensions/LanguageExtensions.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.Globalization;
using osu.Game.Localisation;
diff --git a/osu.Game/Extensions/TimeDisplayExtensions.cs b/osu.Game/Extensions/TimeDisplayExtensions.cs
index 94871233ed..98633958ee 100644
--- a/osu.Game/Extensions/TimeDisplayExtensions.cs
+++ b/osu.Game/Extensions/TimeDisplayExtensions.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 Humanizer;
using osu.Framework.Extensions.LocalisationExtensions;
diff --git a/osu.Game/Extensions/TypeExtensions.cs b/osu.Game/Extensions/TypeExtensions.cs
index 6f160b0479..072b18b0ba 100644
--- a/osu.Game/Extensions/TypeExtensions.cs
+++ b/osu.Game/Extensions/TypeExtensions.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;
@@ -23,7 +21,7 @@ namespace osu.Game.Extensions
///
internal static string GetInvariantInstantiationInfo(this Type type)
{
- string assemblyQualifiedName = type.AssemblyQualifiedName;
+ string? assemblyQualifiedName = type.AssemblyQualifiedName;
if (assemblyQualifiedName == null)
throw new ArgumentException($"{type}'s assembly-qualified name is null. Ensure that it is a concrete type and not a generic type parameter.", nameof(type));
diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs
index 79115c6023..a80b79f259 100644
--- a/osu.Game/Extensions/WebRequestExtensions.cs
+++ b/osu.Game/Extensions/WebRequestExtensions.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.Globalization;
using Newtonsoft.Json.Linq;
using osu.Framework.IO.Network;
@@ -18,7 +16,7 @@ namespace osu.Game.Extensions
///
public static void AddCursor(this WebRequest webRequest, Cursor cursor)
{
- cursor?.Properties.ForEach(x =>
+ cursor.Properties.ForEach(x =>
{
webRequest.AddParameter("cursor[" + x.Key + "]", (x.Value as JValue)?.ToString(CultureInfo.InvariantCulture) ?? x.Value.ToString());
});
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/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
index 45a935d165..4b40add87f 100644
--- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
+++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Graphics.Containers
TimingControlPoint timingPoint;
EffectControlPoint effectPoint;
- IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true;
+ IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null;
double currentTrackTime;
diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs
index 4544633318..10ed76ebdd 100644
--- a/osu.Game/Graphics/Cursor/MenuCursor.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursor.cs
@@ -51,12 +51,16 @@ namespace osu.Game.Graphics.Cursor
{
if (dragRotationState != DragRotationState.NotDragging)
{
+ // make the rotation centre point floating.
+ if (Vector2.Distance(positionMouseDown, e.MousePosition) > 60)
+ positionMouseDown = Interpolation.ValueAt(0.005f, positionMouseDown, e.MousePosition, 0, Clock.ElapsedFrameTime);
+
var position = e.MousePosition;
float distance = Vector2Extensions.Distance(position, positionMouseDown);
// don't start rotating until we're moved a minimum distance away from the mouse down location,
// else it can have an annoying effect.
- if (dragRotationState == DragRotationState.DragStarted && distance > 30)
+ if (dragRotationState == DragRotationState.DragStarted && distance > 80)
dragRotationState = DragRotationState.Rotating;
// don't rotate when distance is zero to avoid NaN
@@ -71,7 +75,7 @@ namespace osu.Game.Graphics.Cursor
if (diff > 180) diff -= 360;
degrees = activeCursor.Rotation + diff;
- activeCursor.RotateTo(degrees, 600, Easing.OutQuint);
+ activeCursor.RotateTo(degrees, 120, Easing.OutQuint);
}
}
@@ -111,7 +115,7 @@ namespace osu.Game.Graphics.Cursor
if (dragRotationState != DragRotationState.NotDragging)
{
- activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf);
+ activeCursor.RotateTo(0, 400 * (0.5f + Math.Abs(activeCursor.Rotation / 960)), Easing.OutElasticQuarter);
dragRotationState = DragRotationState.NotDragging;
}
diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs
index 249fa2fbb2..7a3e54ddf1 100644
--- a/osu.Game/Graphics/UserInterface/Nub.cs
+++ b/osu.Game/Graphics/UserInterface/Nub.cs
@@ -19,7 +19,7 @@ using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface
{
- public class Nub : CompositeDrawable, IHasCurrentValue, IHasAccentColour
+ public class Nub : Container, IHasCurrentValue, IHasAccentColour
{
public const float HEIGHT = 15;
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index 3356153e17..c48627bd21 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -38,8 +38,8 @@ namespace osu.Game.Graphics.UserInterface
private T lastSampleValue;
protected readonly Nub Nub;
- private readonly Box leftBox;
- private readonly Box rightBox;
+ protected readonly Box LeftBox;
+ protected readonly Box RightBox;
private readonly Container nubContainer;
public virtual LocalisableString TooltipText { get; private set; }
@@ -57,7 +57,7 @@ namespace osu.Game.Graphics.UserInterface
set
{
accentColour = value;
- leftBox.Colour = value;
+ LeftBox.Colour = value;
}
}
@@ -69,7 +69,7 @@ namespace osu.Game.Graphics.UserInterface
set
{
backgroundColour = value;
- rightBox.Colour = value;
+ RightBox.Colour = value;
}
}
@@ -96,7 +96,7 @@ namespace osu.Game.Graphics.UserInterface
CornerRadius = 5f,
Children = new Drawable[]
{
- leftBox = new Box
+ LeftBox = new Box
{
Height = 5,
EdgeSmoothness = new Vector2(0, 0.5f),
@@ -104,14 +104,13 @@ namespace osu.Game.Graphics.UserInterface
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
- rightBox = new Box
+ RightBox = new Box
{
Height = 5,
EdgeSmoothness = new Vector2(0, 0.5f),
RelativeSizeAxes = Axes.None,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- Alpha = 0.5f,
},
},
},
@@ -137,7 +136,7 @@ namespace osu.Game.Graphics.UserInterface
{
sample = audio.Samples.Get(@"UI/notch-tick");
AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
- BackgroundColour = colourProvider?.Background5 ?? colours.Pink.Opacity(0.5f);
+ BackgroundColour = colourProvider?.Background5 ?? colours.PinkDarker.Darken(1);
}
protected override void Update()
@@ -165,6 +164,9 @@ namespace osu.Game.Graphics.UserInterface
base.OnHoverLost(e);
}
+ protected override bool ShouldHandleAsRelativeDrag(MouseDownEvent e)
+ => Nub.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition);
+
protected override void OnDragEnd(DragEndEvent e)
{
updateGlow();
@@ -226,9 +228,9 @@ namespace osu.Game.Graphics.UserInterface
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
- leftBox.Scale = new Vector2(Math.Clamp(
+ LeftBox.Scale = new Vector2(Math.Clamp(
RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1);
- rightBox.Scale = new Vector2(Math.Clamp(
+ RightBox.Scale = new Vector2(Math.Clamp(
DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, DrawWidth), 1);
}
diff --git a/osu.Game/IO/LineBufferedReader.cs b/osu.Game/IO/LineBufferedReader.cs
index db435576bf..da1cdba73b 100644
--- a/osu.Game/IO/LineBufferedReader.cs
+++ b/osu.Game/IO/LineBufferedReader.cs
@@ -1,10 +1,7 @@
// 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.IO;
using System.Text;
@@ -17,34 +14,31 @@ namespace osu.Game.IO
public class LineBufferedReader : IDisposable
{
private readonly StreamReader streamReader;
- private readonly Queue lineBuffer;
+
+ private string? peekedLine;
public LineBufferedReader(Stream stream, bool leaveOpen = false)
{
streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen);
- lineBuffer = new Queue();
}
///
/// Reads the next line from the stream without consuming it.
/// Subsequent calls to without a will return the same string.
///
- public string PeekLine()
- {
- if (lineBuffer.Count > 0)
- return lineBuffer.Peek();
-
- string line = streamReader.ReadLine();
- if (line != null)
- lineBuffer.Enqueue(line);
- return line;
- }
+ public string? PeekLine() => peekedLine ??= streamReader.ReadLine();
///
/// Reads the next line from the stream and consumes it.
/// If a line was peeked, that same line will then be consumed and returned.
///
- public string ReadLine() => lineBuffer.Count > 0 ? lineBuffer.Dequeue() : streamReader.ReadLine();
+ public string? ReadLine()
+ {
+ string? line = peekedLine ?? streamReader.ReadLine();
+
+ peekedLine = null;
+ return line;
+ }
///
/// Reads the stream to its end and returns the text read.
@@ -53,14 +47,13 @@ namespace osu.Game.IO
public string ReadToEnd()
{
string remainingText = streamReader.ReadToEnd();
- if (lineBuffer.Count == 0)
+ if (peekedLine == null)
return remainingText;
var builder = new StringBuilder();
// this might not be completely correct due to varying platform line endings
- while (lineBuffer.Count > 0)
- builder.AppendLine(lineBuffer.Dequeue());
+ builder.AppendLine(peekedLine);
builder.Append(remainingText);
return builder.ToString();
@@ -68,7 +61,7 @@ namespace osu.Game.IO
public void Dispose()
{
- streamReader?.Dispose();
+ streamReader.Dispose();
}
}
}
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 89bdd09f0d..368ac56850 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -94,6 +94,8 @@ namespace osu.Game.IO
error = OsuStorageError.None;
Storage lastStorage = UnderlyingStorage;
+ Logger.Log($"Attempting to use custom storage location {CustomStoragePath}");
+
try
{
Storage userStorage = host.GetStorage(CustomStoragePath);
@@ -102,6 +104,7 @@ namespace osu.Game.IO
error = OsuStorageError.AccessibleButEmpty;
ChangeTargetStorage(userStorage);
+ Logger.Log($"Storage successfully changed to {CustomStoragePath}.");
}
catch
{
@@ -109,6 +112,9 @@ namespace osu.Game.IO
ChangeTargetStorage(lastStorage);
}
+ if (error != OsuStorageError.None)
+ Logger.Log($"Custom storage location could not be used ({error}).");
+
return error == OsuStorageError.None;
}
diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs
index 0dc95da2f4..0f0f560df9 100644
--- a/osu.Game/Localisation/AudioSettingsStrings.cs
+++ b/osu.Game/Localisation/AudioSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs
index 417cf335e0..632a1ad0ea 100644
--- a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs
+++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/BindingSettingsStrings.cs b/osu.Game/Localisation/BindingSettingsStrings.cs
index 39b5ac0d21..ad4a650a1f 100644
--- a/osu.Game/Localisation/BindingSettingsStrings.cs
+++ b/osu.Game/Localisation/BindingSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/ButtonSystemStrings.cs b/osu.Game/Localisation/ButtonSystemStrings.cs
index c71a99711b..ba4abf63a6 100644
--- a/osu.Game/Localisation/ButtonSystemStrings.cs
+++ b/osu.Game/Localisation/ButtonSystemStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs
index b07ed18f7b..7bd284a94e 100644
--- a/osu.Game/Localisation/ChatStrings.cs
+++ b/osu.Game/Localisation/ChatStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs
index 39dc7cf518..1ee562e122 100644
--- a/osu.Game/Localisation/CommonStrings.cs
+++ b/osu.Game/Localisation/CommonStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/DebugLocalisationStore.cs b/osu.Game/Localisation/DebugLocalisationStore.cs
index 83ae4581a8..2b114b1bd8 100644
--- a/osu.Game/Localisation/DebugLocalisationStore.cs
+++ b/osu.Game/Localisation/DebugLocalisationStore.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.Globalization;
diff --git a/osu.Game/Localisation/DebugSettingsStrings.cs b/osu.Game/Localisation/DebugSettingsStrings.cs
index e6de4ddee9..74b2c8d892 100644
--- a/osu.Game/Localisation/DebugSettingsStrings.cs
+++ b/osu.Game/Localisation/DebugSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs b/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs
index 4bcbdcff7b..952ca22678 100644
--- a/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs
+++ b/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
index 38f5860cc5..deac7d8628 100644
--- a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
+++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs
index 331a8c6764..3a7fe4bb12 100644
--- a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs
+++ b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs
index 0c73d7a85b..91b427e2ca 100644
--- a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs
+++ b/osu.Game/Localisation/FirstRunSetupOverlayStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs
index 1719b7d6e8..8a0f773551 100644
--- a/osu.Game/Localisation/GameplaySettingsStrings.cs
+++ b/osu.Game/Localisation/GameplaySettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs
index 8cf26a930d..2aa91f5245 100644
--- a/osu.Game/Localisation/GeneralSettingsStrings.cs
+++ b/osu.Game/Localisation/GeneralSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index d45fba6f17..82d03dbb5b 100644
--- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
+++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs
index a73d67067e..38355d9041 100644
--- a/osu.Game/Localisation/GraphicsSettingsStrings.cs
+++ b/osu.Game/Localisation/GraphicsSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs
index a16dcd998a..e46b4cecf3 100644
--- a/osu.Game/Localisation/InputSettingsStrings.cs
+++ b/osu.Game/Localisation/InputSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/JoystickSettingsStrings.cs b/osu.Game/Localisation/JoystickSettingsStrings.cs
index efda1afd48..976ec1adde 100644
--- a/osu.Game/Localisation/JoystickSettingsStrings.cs
+++ b/osu.Game/Localisation/JoystickSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs
index f9094b9540..c13a1a10cb 100644
--- a/osu.Game/Localisation/Language.cs
+++ b/osu.Game/Localisation/Language.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.ComponentModel;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/LayoutSettingsStrings.cs b/osu.Game/Localisation/LayoutSettingsStrings.cs
index 1a0b015050..b4326b8e39 100644
--- a/osu.Game/Localisation/LayoutSettingsStrings.cs
+++ b/osu.Game/Localisation/LayoutSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/LeaderboardStrings.cs b/osu.Game/Localisation/LeaderboardStrings.cs
index 14bc5b7af4..8e53f8e88c 100644
--- a/osu.Game/Localisation/LeaderboardStrings.cs
+++ b/osu.Game/Localisation/LeaderboardStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs
index 4cb514feb0..7a04bcd1ca 100644
--- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs
+++ b/osu.Game/Localisation/MaintenanceSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs
index 3d99075922..e9af7147e3 100644
--- a/osu.Game/Localisation/ModSelectOverlayStrings.cs
+++ b/osu.Game/Localisation/ModSelectOverlayStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs
index 563d6f7637..fd7225ad2e 100644
--- a/osu.Game/Localisation/MouseSettingsStrings.cs
+++ b/osu.Game/Localisation/MouseSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs b/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs
index 956d5195e7..92cedce3e0 100644
--- a/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs
+++ b/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/NamedOverlayComponentStrings.cs b/osu.Game/Localisation/NamedOverlayComponentStrings.cs
index f6a50460dc..475bea2a4a 100644
--- a/osu.Game/Localisation/NamedOverlayComponentStrings.cs
+++ b/osu.Game/Localisation/NamedOverlayComponentStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs
index c60e5d3415..382e0d81f4 100644
--- a/osu.Game/Localisation/NotificationsStrings.cs
+++ b/osu.Game/Localisation/NotificationsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/NowPlayingStrings.cs b/osu.Game/Localisation/NowPlayingStrings.cs
index 75d9b28ea6..f334637338 100644
--- a/osu.Game/Localisation/NowPlayingStrings.cs
+++ b/osu.Game/Localisation/NowPlayingStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs
index ebb21afeb7..6862f4ac2c 100644
--- a/osu.Game/Localisation/OnlineSettingsStrings.cs
+++ b/osu.Game/Localisation/OnlineSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs
index 08901a641e..a356c9e20b 100644
--- a/osu.Game/Localisation/RulesetSettingsStrings.cs
+++ b/osu.Game/Localisation/RulesetSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/SettingsStrings.cs b/osu.Game/Localisation/SettingsStrings.cs
index 5dcab39e10..aa2e2740eb 100644
--- a/osu.Game/Localisation/SettingsStrings.cs
+++ b/osu.Game/Localisation/SettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs
index 42b72fdbb8..81035c5a5e 100644
--- a/osu.Game/Localisation/SkinSettingsStrings.cs
+++ b/osu.Game/Localisation/SkinSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/TabletSettingsStrings.cs b/osu.Game/Localisation/TabletSettingsStrings.cs
index 8d2bc21652..d62d348df9 100644
--- a/osu.Game/Localisation/TabletSettingsStrings.cs
+++ b/osu.Game/Localisation/TabletSettingsStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs
index 00a0031293..52e75425bf 100644
--- a/osu.Game/Localisation/ToastStrings.cs
+++ b/osu.Game/Localisation/ToastStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs
index a007f760d8..a090b8c14c 100644
--- a/osu.Game/Localisation/UserInterfaceStrings.cs
+++ b/osu.Game/Localisation/UserInterfaceStrings.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.Framework.Localisation;
namespace osu.Game.Localisation
diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs
index 7104245d9e..c751530bf4 100644
--- a/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs
+++ b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs
@@ -1,7 +1,5 @@
//
using Microsoft.EntityFrameworkCore;
-
-#nullable disable
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.cs b/osu.Game/Migrations/20171019041408_InitialCreate.cs
index f1c7c94638..08ab64fd08 100644
--- a/osu.Game/Migrations/20171019041408_InitialCreate.cs
+++ b/osu.Game/Migrations/20171019041408_InitialCreate.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs
index b464c15ce2..4cd234f2ef 100644
--- a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs
+++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs
@@ -1,7 +1,5 @@
//
using Microsoft.EntityFrameworkCore;
-
-#nullable disable
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs
index 1d0164a1d9..4ec3952941 100644
--- a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs
+++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs
index 53e8b887d8..006acf12cd 100644
--- a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs
+++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs
@@ -1,7 +1,5 @@
//
using Microsoft.EntityFrameworkCore;
-
-#nullable disable
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs
index 765a6a5b58..6aba12f86f 100644
--- a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs
+++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs
index 4da20141bc..fc2496bc24 100644
--- a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs
+++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs
@@ -1,7 +1,5 @@
//
using Microsoft.EntityFrameworkCore;
-
-#nullable disable
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs
index 3379efa68e..5688455f79 100644
--- a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs
+++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20180125143340_Settings.Designer.cs b/osu.Game/Migrations/20180125143340_Settings.Designer.cs
index 68054d6404..4bb599eec1 100644
--- a/osu.Game/Migrations/20180125143340_Settings.Designer.cs
+++ b/osu.Game/Migrations/20180125143340_Settings.Designer.cs
@@ -1,7 +1,5 @@
//
using Microsoft.EntityFrameworkCore;
-
-#nullable disable
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20180125143340_Settings.cs b/osu.Game/Migrations/20180125143340_Settings.cs
index 8f7d0a6ed3..1feb37531f 100644
--- a/osu.Game/Migrations/20180125143340_Settings.cs
+++ b/osu.Game/Migrations/20180125143340_Settings.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs b/osu.Game/Migrations/20180131154205_AddMuteBinding.cs
index 3e97e78e61..8646d1d76b 100644
--- a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs
+++ b/osu.Game/Migrations/20180131154205_AddMuteBinding.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 Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using osu.Game.Database;
diff --git a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs
index 7d95a702b8..cdc4ef2e66 100644
--- a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs
+++ b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs
@@ -1,7 +1,5 @@
//
using Microsoft.EntityFrameworkCore;
-
-#nullable disable
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20180219060912_AddSkins.cs b/osu.Game/Migrations/20180219060912_AddSkins.cs
index df9d267a14..319748bed6 100644
--- a/osu.Game/Migrations/20180219060912_AddSkins.cs
+++ b/osu.Game/Migrations/20180219060912_AddSkins.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs
index 7e490c7833..f28408bfb3 100644
--- a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs
+++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs
@@ -1,7 +1,5 @@
//
using Microsoft.EntityFrameworkCore;
-
-#nullable disable
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs
index 62c341b0c6..91eabe8868 100644
--- a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs
+++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs
index a103cdad20..aaa11e88b6 100644
--- a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs
+++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs
index b08fa7a899..d888ccd5a2 100644
--- a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs
+++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs
index 5b1734554a..7eeacd56d7 100644
--- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs
+++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs
index 4ba0a4d0e6..fdea636ac6 100644
--- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs
+++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs
index ceef5b6948..5ab43da046 100644
--- a/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs
+++ b/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.cs
index ec82925b03..bb147dff84 100644
--- a/osu.Game/Migrations/20180913080842_AddRankStatus.cs
+++ b/osu.Game/Migrations/20180913080842_AddRankStatus.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs
index d1185ef186..b387a45ecf 100644
--- a/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs
+++ b/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.cs
index f9323c0b9b..30f27043a0 100644
--- a/osu.Game/Migrations/20181007180454_StandardizePaths.cs
+++ b/osu.Game/Migrations/20181007180454_StandardizePaths.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
index 0797090c7b..120674671a 100644
--- a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
+++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs
index 43689f2a3c..ee825a1e9c 100644
--- a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs
+++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs
index 3b70ad8d45..eee53182ce 100644
--- a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs
+++ b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs
index f52edacc7f..58980132f3 100644
--- a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs
+++ b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.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 Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs
index 51b63bd9eb..8e1e3a59f3 100644
--- a/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs
+++ b/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs
index 00d807d5c4..f2eef600dc 100644
--- a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs
+++ b/osu.Game/Migrations/20190225062029_AddUserIDColumn.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs
index 9d6515e5c6..348c42adb9 100644
--- a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs
+++ b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.cs b/osu.Game/Migrations/20190525060824_SkinSettings.cs
index 463d4fe1ad..7779b55bb7 100644
--- a/osu.Game/Migrations/20190525060824_SkinSettings.cs
+++ b/osu.Game/Migrations/20190525060824_SkinSettings.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs
index 1bc2e76ae5..9477369aa0 100644
--- a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs
+++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs
index 61925a4cb4..0620a0624f 100644
--- a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs
+++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.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 Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs
index c51b8739bc..c5fcc16f84 100644
--- a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs
+++ b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs
index 19a2464157..f8ce354aa1 100644
--- a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs
+++ b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs
index 95583e7587..826233a2b0 100644
--- a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs
+++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs
index c1208e0bf1..af82b4db20 100644
--- a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs
+++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs
index 3c7de0602b..22316b0380 100644
--- a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs
+++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs
index 73cfb1725b..3d2ddbf6fc 100644
--- a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs
+++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs
index 24acc3195f..1c05de832e 100644
--- a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs
+++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs
index eaca8785f9..58a35a7bf3 100644
--- a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs
+++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
index 704af354c9..2c100d39b9 100644
--- a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
+++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs
index 7772c0e86b..4d3941dd20 100644
--- a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs
+++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs
index 097bd6a244..b808c648da 100644
--- a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs
+++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs
index 1cc9ab6120..887635fa85 100644
--- a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs
+++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs
index ca9502422b..89bab3a0fa 100644
--- a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs
+++ b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs
index 82c5b27769..7b579e27b9 100644
--- a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs
+++ b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs
index 2a4e370649..afeb42130d 100644
--- a/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs
+++ b/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs
index dd6fa23387..d1b09e2c1d 100644
--- a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs
+++ b/osu.Game/Migrations/20210824185035_AddCountdownSettings.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs
index 9e09f2c69e..6e53d7fae0 100644
--- a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs
+++ b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs
index 0598a41f09..f6fc1f4420 100644
--- a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs
+++ b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.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 Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
diff --git a/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs b/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs
index 0f7a2a5702..6d53c019ec 100644
--- a/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs
+++ b/osu.Game/Migrations/20211020081609_ResetSkinHashes.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 Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using osu.Game.Database;
diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
index 7e38889fa6..036c26cb0a 100644
--- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
+++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
@@ -1,7 +1,5 @@
//
using System;
-
-#nullable disable
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs
index 0f6f439d73..c4310c4edb 100644
--- a/osu.Game/Models/RealmNamedFileUsage.cs
+++ b/osu.Game/Models/RealmNamedFileUsage.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using JetBrains.Annotations;
using osu.Framework.Testing;
using osu.Game.Database;
@@ -19,8 +20,8 @@ namespace osu.Game.Models
public RealmNamedFileUsage(RealmFile file, string filename)
{
- File = file;
- Filename = filename;
+ File = file ?? throw new ArgumentNullException(nameof(file));
+ Filename = filename ?? throw new ArgumentNullException(nameof(filename));
}
[UsedImplicitly]
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 088dc56701..43cea7fb97 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Online.API
public string WebsiteRootUrl { get; }
- public int APIVersion => 20220217; // We may want to pull this from the game version eventually.
+ public int APIVersion => 20220705; // We may want to pull this from the game version eventually.
public Exception LastLoginError { get; private set; }
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 245760a00a..07d544260e 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -15,10 +15,12 @@ namespace osu.Game.Online.API
{
public class DummyAPIAccess : Component, IAPIProvider
{
+ public const int DUMMY_USER_ID = 1001;
+
public Bindable LocalUser { get; } = new Bindable(new APIUser
{
Username = @"Dummy",
- Id = 1001,
+ Id = DUMMY_USER_ID,
});
public BindableList Friends { get; } = new BindableList();
diff --git a/osu.Game/Online/API/Requests/GetNewsRequest.cs b/osu.Game/Online/API/Requests/GetNewsRequest.cs
index e1c9eefe30..64bed344bb 100644
--- a/osu.Game/Online/API/Requests/GetNewsRequest.cs
+++ b/osu.Game/Online/API/Requests/GetNewsRequest.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.Framework.IO.Network;
using osu.Game.Extensions;
@@ -11,9 +9,9 @@ namespace osu.Game.Online.API.Requests
public class GetNewsRequest : APIRequest
{
private readonly int? year;
- private readonly Cursor cursor;
+ private readonly Cursor? cursor;
- public GetNewsRequest(int? year = null, Cursor cursor = null)
+ public GetNewsRequest(int? year = null, Cursor? cursor = null)
{
this.year = year;
this.cursor = cursor;
@@ -22,7 +20,9 @@ namespace osu.Game.Online.API.Requests
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
- req.AddCursor(cursor);
+
+ if (cursor != null)
+ req.AddCursor(cursor);
if (year.HasValue)
req.AddParameter("year", year.Value.ToString());
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
index 258708de2b..735fde333d 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs
@@ -69,6 +69,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"count_sliders")]
public int SliderCount { get; set; }
+ [JsonProperty(@"count_spinners")]
+ public int SpinnerCount { get; set; }
+
[JsonProperty(@"version")]
public string DifficultyName { get; set; } = string.Empty;
diff --git a/osu.Game/Online/API/Requests/Responses/APIScore.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs
index 1b56362aec..f236607761 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScore.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs
@@ -88,7 +88,7 @@ namespace osu.Game.Online.API.Requests.Responses
///
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
- var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException();
+ var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally");
var rulesetInstance = ruleset.CreateInstance();
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
index 8bd54f889d..494826f534 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
@@ -16,11 +16,11 @@ namespace osu.Game.Online.API.Requests.Responses
public int? Position;
[JsonProperty(@"score")]
- public APIScore Score;
+ public SoloScoreInfo Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
- var score = Score.CreateScoreInfo(rulesets, beatmap);
+ var score = Score.ToScoreInfo(rulesets, beatmap);
score.Position = Position;
return score;
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
index 9c8a38c63a..38c67d92f4 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIScoresCollection
{
[JsonProperty(@"scores")]
- public List Scores;
+ public List Scores;
[JsonProperty(@"userScore")]
public APIScoreWithPosition UserScore;
diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
new file mode 100644
index 0000000000..b70da194a5
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
@@ -0,0 +1,143 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ [Serializable]
+ public class SoloScoreInfo : IHasOnlineID
+ {
+ [JsonProperty("replay")]
+ public bool HasReplay { get; set; }
+
+ [JsonProperty("beatmap_id")]
+ public int BeatmapID { get; set; }
+
+ [JsonProperty("ruleset_id")]
+ public int RulesetID { get; set; }
+
+ [JsonProperty("build_id")]
+ public int? BuildID { get; set; }
+
+ [JsonProperty("passed")]
+ public bool Passed { get; set; }
+
+ [JsonProperty("total_score")]
+ public int TotalScore { get; set; }
+
+ [JsonProperty("accuracy")]
+ public double Accuracy { get; set; }
+
+ [JsonProperty("user_id")]
+ public int UserID { get; set; }
+
+ // TODO: probably want to update this column to match user stats (short)?
+ [JsonProperty("max_combo")]
+ public int MaxCombo { get; set; }
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ [JsonProperty("rank")]
+ public ScoreRank Rank { get; set; }
+
+ [JsonProperty("started_at")]
+ public DateTimeOffset? StartedAt { get; set; }
+
+ [JsonProperty("ended_at")]
+ public DateTimeOffset? EndedAt { get; set; }
+
+ [JsonProperty("mods")]
+ public APIMod[] Mods { get; set; } = Array.Empty();
+
+ [JsonIgnore]
+ [JsonProperty("created_at")]
+ public DateTimeOffset CreatedAt { get; set; }
+
+ [JsonIgnore]
+ [JsonProperty("updated_at")]
+ public DateTimeOffset UpdatedAt { get; set; }
+
+ [JsonIgnore]
+ [JsonProperty("deleted_at")]
+ public DateTimeOffset? DeletedAt { get; set; }
+
+ [JsonProperty("statistics")]
+ public Dictionary Statistics { get; set; } = new Dictionary();
+
+ #region osu-web API additions (not stored to database).
+
+ [JsonProperty("id")]
+ public long? ID { get; set; }
+
+ [JsonProperty("user")]
+ public APIUser? User { get; set; }
+
+ [JsonProperty("pp")]
+ public double? PP { get; set; }
+
+ #endregion
+
+ public override string ToString() => $"score_id: {ID} user_id: {UserID}";
+
+ ///
+ /// Create a from an API score instance.
+ ///
+ /// A ruleset store, used to populate a ruleset instance in the returned score.
+ /// An optional beatmap, copied into the returned score (for cases where the API does not populate the beatmap).
+ ///
+ public ScoreInfo ToScoreInfo(RulesetStore rulesets, BeatmapInfo? beatmap = null)
+ {
+ var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally");
+
+ var rulesetInstance = ruleset.CreateInstance();
+
+ var mods = Mods.Select(apiMod => rulesetInstance.CreateModFromAcronym(apiMod.Acronym)).Where(m => m != null).ToArray();
+
+ // all API scores provided by this class are considered to be legacy.
+ mods = mods.Append(rulesetInstance.CreateMod()).ToArray();
+
+ var scoreInfo = ToScoreInfo(mods);
+
+ scoreInfo.Ruleset = ruleset;
+ if (beatmap != null) scoreInfo.BeatmapInfo = beatmap;
+
+ return scoreInfo;
+ }
+
+ ///
+ /// Create a from an API score instance.
+ ///
+ /// The mod instances, resolved from a ruleset.
+ ///
+ public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo
+ {
+ OnlineID = OnlineID,
+ User = User ?? new APIUser { Id = UserID },
+ BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
+ Ruleset = new RulesetInfo { OnlineID = RulesetID },
+ Passed = Passed,
+ TotalScore = TotalScore,
+ Accuracy = Accuracy,
+ MaxCombo = MaxCombo,
+ Rank = Rank,
+ Statistics = Statistics,
+ Date = EndedAt ?? DateTimeOffset.Now,
+ Hash = "online", // TODO: temporary?
+ HasReplay = HasReplay,
+ Mods = mods,
+ PP = PP,
+ };
+
+ public long OnlineID => ID ?? -1;
+ }
+}
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 73f6fce4f9..082f9bb371 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -112,7 +112,8 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("nsfw", ExplicitContent == SearchExplicit.Show ? "true" : "false");
- req.AddCursor(cursor);
+ if (cursor != null)
+ req.AddCursor(cursor);
return req;
}
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 48dfaadfa5..ec84b0643d 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -133,12 +133,14 @@ namespace osu.Game.Online.Chat
?? JoinChannel(new Channel(user));
}
- private void currentChannelChanged(ValueChangedEvent e)
+ private void currentChannelChanged(ValueChangedEvent channel)
{
- bool isSelectorChannel = e.NewValue is ChannelListing.ChannelListingChannel;
+ bool isSelectorChannel = channel.NewValue is ChannelListing.ChannelListingChannel;
if (!isSelectorChannel)
- JoinChannel(e.NewValue);
+ JoinChannel(channel.NewValue);
+
+ Logger.Log($"Current channel changed to {channel.NewValue}");
}
///
@@ -447,9 +449,17 @@ namespace osu.Game.Online.Chat
return channel;
case ChannelType.PM:
+ Logger.Log($"Attempting to join PM channel {channel}");
+
var createRequest = new CreateChannelRequest(channel);
+ createRequest.Failure += e =>
+ {
+ Logger.Log($"Failed to join PM channel {channel} ({e.Message})");
+ };
createRequest.Success += resChannel =>
{
+ Logger.Log($"Joined PM channel {channel} ({resChannel.ChannelID})");
+
if (resChannel.ChannelID.HasValue)
{
channel.Id = resChannel.ChannelID.Value;
@@ -463,9 +473,19 @@ namespace osu.Game.Online.Chat
break;
default:
+ Logger.Log($"Attempting to join public channel {channel}");
+
var req = new JoinChannelRequest(channel);
- req.Success += () => joinChannel(channel, fetchInitialMessages);
- req.Failure += _ => LeaveChannel(channel);
+ req.Success += () =>
+ {
+ Logger.Log($"Joined public channel {channel}");
+ joinChannel(channel, fetchInitialMessages);
+ };
+ req.Failure += e =>
+ {
+ Logger.Log($"Failed to join public channel {channel} ({e.Message})");
+ LeaveChannel(channel);
+ };
api.Queue(req);
return channel;
}
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/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index e32bc63aa1..62827f50aa 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -426,10 +426,10 @@ namespace osu.Game.Online.Leaderboards
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (Score.Files.Count > 0)
+ {
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score)));
-
- if (!isOnlineScope)
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
+ }
return items.ToArray();
}
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/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 22f474ed42..9832acb140 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -413,7 +413,7 @@ namespace osu.Game.Online.Multiplayer
UserJoined?.Invoke(user);
RoomUpdated?.Invoke();
- });
+ }, false);
}
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) =>
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/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs
index 13f14bca5b..6f597e5b10 100644
--- a/osu.Game/Online/Rooms/MultiplayerScore.cs
+++ b/osu.Game/Online/Rooms/MultiplayerScore.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Online.Rooms
TotalScore = TotalScore,
MaxCombo = MaxCombo,
BeatmapInfo = beatmap,
- Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException(),
+ Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"),
Statistics = Statistics,
User = User,
Accuracy = Accuracy,
diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs
index a0711d9ded..2213311c67 100644
--- a/osu.Game/Online/Rooms/PlaylistItem.cs
+++ b/osu.Game/Online/Rooms/PlaylistItem.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Online.Rooms
/// In many cases, this will *not* contain any usable information apart from OnlineID.
///
[JsonIgnore]
- public IBeatmapInfo Beatmap { get; set; } = null!;
+ public IBeatmapInfo Beatmap { get; private set; }
[JsonIgnore]
public IBindable Valid => valid;
@@ -81,6 +81,7 @@ namespace osu.Game.Online.Rooms
[JsonConstructor]
private PlaylistItem()
+ : this(new APIBeatmap())
{
}
diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs
index e4612b6659..8c92b32915 100644
--- a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs
+++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs
@@ -6,6 +6,7 @@
using System.Globalization;
using System.Net.Http;
using osu.Framework.IO.Network;
+using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -13,13 +14,13 @@ namespace osu.Game.Online.Solo
{
public class CreateSoloScoreRequest : APIRequest
{
- private readonly int beatmapId;
+ private readonly BeatmapInfo beatmapInfo;
private readonly int rulesetId;
private readonly string versionHash;
- public CreateSoloScoreRequest(int beatmapId, int rulesetId, string versionHash)
+ public CreateSoloScoreRequest(BeatmapInfo beatmapInfo, int rulesetId, string versionHash)
{
- this.beatmapId = beatmapId;
+ this.beatmapInfo = beatmapInfo;
this.rulesetId = rulesetId;
this.versionHash = versionHash;
}
@@ -29,10 +30,11 @@ namespace osu.Game.Online.Solo
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter("version_hash", versionHash);
+ req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash);
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
return req;
}
- protected override string Target => $@"beatmaps/{beatmapId}/solo/scores";
+ protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/solo/scores";
}
}
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 4a43aa6c66..68c8b57019 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -188,7 +188,10 @@ namespace osu.Game.Online.Spectator
}
if (frame is IConvertibleReplayFrame convertible)
+ {
+ Debug.Assert(currentBeatmap != null);
pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap));
+ }
if (pendingFrames.Count > max_pending_frames)
purgePendingFrames();
@@ -294,17 +297,21 @@ namespace osu.Game.Online.Spectator
lastSend = tcs.Task;
- SendFramesInternal(bundle).ContinueWith(t => Schedule(() =>
+ SendFramesInternal(bundle).ContinueWith(t =>
{
+ // Handle exception outside of `Schedule` to ensure it doesn't go unovserved.
bool wasSuccessful = t.Exception == null;
- // If the last bundle send wasn't successful, try again without dequeuing.
- if (wasSuccessful)
- pendingFrameBundles.Dequeue();
+ return Schedule(() =>
+ {
+ // If the last bundle send wasn't successful, try again without dequeuing.
+ if (wasSuccessful)
+ pendingFrameBundles.Dequeue();
- tcs.SetResult(wasSuccessful);
- sendNextBundleIfRequired();
- }));
+ tcs.SetResult(wasSuccessful);
+ sendNextBundleIfRequired();
+ });
+ });
}
}
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index f51a18b36f..bd0a2680ae 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -24,6 +24,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
@@ -231,7 +232,7 @@ namespace osu.Game
///
/// Unregisters a blocking that was not created by itself.
///
- private void unregisterBlockingOverlay(OverlayContainer overlayContainer)
+ private void unregisterBlockingOverlay(OverlayContainer overlayContainer) => Schedule(() =>
{
externalOverlays.Remove(overlayContainer);
@@ -239,7 +240,7 @@ namespace osu.Game
focusedOverlays.Remove(focusedOverlayContainer);
overlayContainer.Expire();
- }
+ });
#endregion
@@ -486,6 +487,7 @@ namespace osu.Game
///
public void PresentBeatmap(IBeatmapSetInfo beatmap, Predicate difficultyCriteria = null)
{
+ Logger.Log($"Beginning {nameof(PresentBeatmap)} with beatmap {beatmap}");
Live databasedSet = null;
if (beatmap.OnlineID > 0)
@@ -522,6 +524,7 @@ namespace osu.Game
}
else
{
+ Logger.Log($"Completing {nameof(PresentBeatmap)} with beatmap {beatmap} ruleset {selection.Ruleset}");
Ruleset.Value = selection.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
}
@@ -534,6 +537,8 @@ namespace osu.Game
///
public void PresentScore(IScoreInfo score, ScorePresentType presentType = ScorePresentType.Results)
{
+ Logger.Log($"Beginning {nameof(PresentScore)} with score {score}");
+
// The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database
// to ensure all the required data for presenting a replay are present.
ScoreInfo databasedScoreInfo = null;
@@ -568,6 +573,8 @@ namespace osu.Game
PerformFromScreen(screen =>
{
+ Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset} to match score");
+
Ruleset.Value = databasedScore.ScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
@@ -612,7 +619,6 @@ namespace osu.Game
{
beatmap.OldValue?.CancelAsyncLoad();
beatmap.NewValue?.BeginAsyncLoad();
- Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}");
}
private void modsChanged(ValueChangedEvent> mods)
@@ -681,27 +687,29 @@ namespace osu.Game
{
base.LoadComplete();
- foreach (var language in Enum.GetValues(typeof(Language)).OfType())
+ var languages = Enum.GetValues(typeof(Language)).OfType();
+
+ var mappings = languages.Select(language =>
{
#if DEBUG
if (language == Language.debug)
- {
- Localisation.AddLanguage(Language.debug.ToString(), new DebugLocalisationStore());
- continue;
- }
+ return new LocaleMapping("debug", new DebugLocalisationStore());
#endif
string cultureCode = language.ToCultureCode();
try
{
- Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
+ return new LocaleMapping(new ResourceManagerLocalisationStore(cultureCode));
}
catch (Exception ex)
{
Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\"");
+ return null;
}
- }
+ }).Where(m => m != null);
+
+ Localisation.AddLocaleMappings(mappings);
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
// in the cursor being shown for a few frames during the intro.
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 7d39aad226..4b5c9c0815 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,16 +267,14 @@ 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, performOnlineLookups: true));
+ dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, API, 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));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, 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);
@@ -446,7 +459,7 @@ namespace osu.Game
Scheduler.Add(() =>
{
- realmBlocker = realm.BlockAllOperations();
+ realmBlocker = realm.BlockAllOperations("migration");
readyToRun.Set();
}, false);
@@ -490,10 +503,12 @@ namespace osu.Game
}
}
- private void onBeatmapChanged(ValueChangedEvent valueChangedEvent)
+ private void onBeatmapChanged(ValueChangedEvent beatmap)
{
if (IsLoaded && !ThreadSafety.IsUpdateThread)
throw new InvalidOperationException("Global beatmap bindable must be changed from update thread.");
+
+ Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}");
}
private void onRulesetChanged(ValueChangedEvent r)
@@ -572,16 +587,17 @@ namespace osu.Game
base.Dispose(isDisposing);
RulesetStore?.Dispose();
- BeatmapManager?.Dispose();
LocalConfig?.Dispose();
+ beatmapUpdater?.Dispose();
+
realm?.Dispose();
if (Host != null)
Host.ExceptionThrown -= onExceptionThrown;
}
- ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.Beatmap.ControlPointInfo;
+ ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null;
IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null;
ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null;
}
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSection.cs b/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
index c6bf94f507..317b369d8f 100644
--- a/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -22,11 +23,14 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly MetadataType type;
private TextFlowContainer textFlow;
+ private readonly Action searchAction;
+
private const float transition_duration = 250;
- public MetadataSection(MetadataType type)
+ public MetadataSection(MetadataType type, Action searchAction = null)
{
this.type = type;
+ this.searchAction = searchAction;
Alpha = 0;
@@ -91,7 +95,12 @@ namespace osu.Game.Overlays.BeatmapSet
for (int i = 0; i <= tags.Length - 1; i++)
{
- loaded.AddLink(tags[i], LinkAction.SearchBeatmapSet, tags[i]);
+ string tag = tags[i];
+
+ if (searchAction != null)
+ loaded.AddLink(tag, () => searchAction(tag));
+ else
+ loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
if (i != tags.Length - 1)
loaded.AddText(" ");
@@ -100,7 +109,11 @@ namespace osu.Game.Overlays.BeatmapSet
break;
case MetadataType.Source:
- loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
+ if (searchAction != null)
+ loaded.AddLink(text, () => searchAction(text));
+ else
+ loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
+
break;
default:
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index c2e54d0d7b..e50fc356eb 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
MD5Hash = apiBeatmap.MD5Hash
};
- scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
+ scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
.ContinueWith(task => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
@@ -101,7 +101,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
scoreTable.Show();
var userScore = value.UserScore;
- var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets, beatmapInfo);
+ var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo);
topScoresContainer.Add(new DrawableTopScore(topScore));
diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs
index a07cf1608d..493cd66258 100644
--- a/osu.Game/Overlays/DialogOverlay.cs
+++ b/osu.Game/Overlays/DialogOverlay.cs
@@ -49,43 +49,54 @@ namespace osu.Game.Overlays
public void Push(PopupDialog dialog)
{
- if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
-
- var lastDialog = CurrentDialog;
+ if (dialog == CurrentDialog || dialog.State.Value == Visibility.Hidden) return;
// Immediately update the externally accessible property as this may be used for checks even before
// a DialogOverlay instance has finished loading.
+ var lastDialog = CurrentDialog;
CurrentDialog = dialog;
- Scheduler.Add(() =>
+ Schedule(() =>
{
// if any existing dialog is being displayed, dismiss it before showing a new one.
lastDialog?.Hide();
- dialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
- dialogContainer.Add(dialog);
+ // if the new dialog is hidden before added to the dialogContainer, bypass any further operations.
+ if (dialog.State.Value == Visibility.Hidden)
+ {
+ dismiss();
+ return;
+ }
+
+ dialogContainer.Add(dialog);
Show();
- }, false);
+
+ dialog.State.BindValueChanged(state =>
+ {
+ if (state.NewValue != Visibility.Hidden) return;
+
+ // Trigger the demise of the dialog as soon as it hides.
+ dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
+
+ dismiss();
+ });
+ });
+
+ void dismiss()
+ {
+ if (dialog != CurrentDialog) return;
+
+ // Handle the case where the dialog is the currently displayed dialog.
+ // In this scenario, the overlay itself should also be hidden.
+ Hide();
+ CurrentDialog = null;
+ }
}
public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0;
protected override bool BlockNonPositionalInput => true;
- private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v)
- {
- if (v != Visibility.Hidden) return;
-
- // handle the dialog being dismissed.
- dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
-
- if (dialog == CurrentDialog)
- {
- Hide();
- CurrentDialog = null;
- }
- }
-
protected override void PopIn()
{
base.PopIn();
@@ -97,7 +108,8 @@ namespace osu.Game.Overlays
base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
- if (CurrentDialog?.State.Value == Visibility.Visible)
+ // PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present.
+ if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible)
CurrentDialog.Hide();
}
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
index 20ff8f21c8..cb1e96d2f2 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
@@ -1,14 +1,24 @@
// 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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Configuration;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Framework.Threading;
+using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
+using osuTK;
namespace osu.Game.Overlays.FirstRunSetup
{
@@ -20,13 +30,175 @@ namespace osu.Game.Overlays.FirstRunSetup
{
Content.Children = new Drawable[]
{
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ RowDimensions = new[]
+ {
+ // Avoid height changes when changing language.
+ new Dimension(GridSizeMode.AutoSize, minSize: 100),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
+ {
+ Text = FirstRunSetupOverlayStrings.WelcomeDescription,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ },
+ },
+ }
+ },
+ new LanguageSelectionFlow
{
- Text = FirstRunSetupOverlayStrings.WelcomeDescription,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
- },
+ }
};
}
+
+ private class LanguageSelectionFlow : FillFlowContainer
+ {
+ private Bindable frameworkLocale = null!;
+
+ private ScheduledDelegate? updateSelectedDelegate;
+
+ [BackgroundDependencyLoader]
+ private void load(FrameworkConfigManager frameworkConfig)
+ {
+ Direction = FillDirection.Full;
+ Spacing = new Vector2(5);
+
+ ChildrenEnumerable = Enum.GetValues(typeof(Language))
+ .Cast()
+ .Select(l => new LanguageButton(l)
+ {
+ Action = () => frameworkLocale.Value = l.ToCultureCode()
+ });
+
+ frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale);
+ frameworkLocale.BindValueChanged(locale =>
+ {
+ if (!LanguageExtensions.TryParseCultureCode(locale.NewValue, out var language))
+ language = Language.en;
+
+ // Changing language may cause a short period of blocking the UI thread while the new glyphs are loaded.
+ // Scheduling ensures the button animation plays smoothly after any blocking operation completes.
+ // Note that a delay is required (the alternative would be a double-schedule; delay feels better).
+ updateSelectedDelegate?.Cancel();
+ updateSelectedDelegate = Scheduler.AddDelayed(() => updateSelectedStates(language), 50);
+ }, true);
+ }
+
+ private void updateSelectedStates(Language language)
+ {
+ foreach (var c in Children.OfType())
+ c.Selected = c.Language == language;
+ }
+
+ private class LanguageButton : OsuClickableContainer
+ {
+ public readonly Language Language;
+
+ private Box backgroundBox = null!;
+
+ private OsuSpriteText text = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ private bool selected;
+
+ public bool Selected
+ {
+ get => selected;
+ set
+ {
+ if (selected == value)
+ return;
+
+ selected = value;
+
+ if (IsLoaded)
+ updateState();
+ }
+ }
+
+ public LanguageButton(Language language)
+ {
+ Language = language;
+
+ Size = new Vector2(160, 50);
+ Masking = true;
+ CornerRadius = 10;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ backgroundBox = new Box
+ {
+ Alpha = 0,
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
+ },
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = colourProvider.Light1,
+ Text = Language.GetDescription(),
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateState();
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ if (!selected)
+ updateState();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ if (!selected)
+ updateState();
+ base.OnHoverLost(e);
+ }
+
+ private void updateState()
+ {
+ if (selected)
+ {
+ const double selected_duration = 1000;
+
+ backgroundBox.FadeTo(1, selected_duration, Easing.OutQuint);
+ backgroundBox.FadeColour(colourProvider.Background2, selected_duration, Easing.OutQuint);
+ text.FadeColour(colourProvider.Content1, selected_duration, Easing.OutQuint);
+ text.ScaleTo(1.2f, selected_duration, Easing.OutQuint);
+ }
+ else
+ {
+ const double duration = 500;
+
+ backgroundBox.FadeTo(IsHovered ? 1 : 0, duration, Easing.OutQuint);
+ backgroundBox.FadeColour(colourProvider.Background5, duration, Easing.OutQuint);
+ text.FadeColour(colourProvider.Light1, duration, Easing.OutQuint);
+ text.ScaleTo(1, duration, Easing.OutQuint);
+ }
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs
index f545e2892f..0042f4607d 100644
--- a/osu.Game/Overlays/Login/LoginForm.cs
+++ b/osu.Game/Overlays/Login/LoginForm.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 osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
@@ -23,19 +21,19 @@ namespace osu.Game.Overlays.Login
{
public class LoginForm : FillFlowContainer
{
- private TextBox username;
- private TextBox password;
- private ShakeContainer shakeSignIn;
+ private TextBox username = null!;
+ private TextBox password = null!;
+ private ShakeContainer shakeSignIn = null!;
- [Resolved(CanBeNull = true)]
- private IAPIProvider api { get; set; }
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
- public Action RequestHide;
+ public Action? RequestHide;
private void performLogin()
{
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
- api?.Login(username.Text, password.Text);
+ api.Login(username.Text, password.Text);
else
shakeSignIn.Shake();
}
@@ -49,6 +47,7 @@ namespace osu.Game.Overlays.Login
RelativeSizeAxes = Axes.X;
ErrorTextFlowContainer errorText;
+ LinkFlowContainer forgottenPaswordLink;
Children = new Drawable[]
{
@@ -56,7 +55,7 @@ namespace osu.Game.Overlays.Login
{
PlaceholderText = UsersStrings.LoginUsername.ToLower(),
RelativeSizeAxes = Axes.X,
- Text = api?.ProvidedUsername ?? string.Empty,
+ Text = api.ProvidedUsername,
TabbableContentContainer = this
},
password = new OsuPasswordTextBox
@@ -80,6 +79,12 @@ namespace osu.Game.Overlays.Login
LabelText = "Stay signed in",
Current = config.GetBindable(OsuSetting.SavePassword),
},
+ forgottenPaswordLink = new LinkFlowContainer
+ {
+ Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
new Container
{
RelativeSizeAxes = Axes.X,
@@ -103,15 +108,17 @@ namespace osu.Game.Overlays.Login
Text = "Register",
Action = () =>
{
- RequestHide();
+ RequestHide?.Invoke();
accountCreation.Show();
}
}
};
+ forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
+
password.OnCommit += (_, _) => performLogin();
- if (api?.LastLoginError?.Message is string error)
+ if (api.LastLoginError?.Message is string error)
errorText.AddErrors(new[] { error });
}
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 69cb3a49fc..04c424461e 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -6,6 +6,8 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -28,6 +30,9 @@ namespace osu.Game.Overlays.Mods
{
public const int BUTTON_WIDTH = 200;
+ protected override string PopInSampleName => "";
+ protected override string PopOutSampleName => @"SongSelect/mod-select-overlay-pop-out";
+
[Cached]
public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty());
@@ -101,17 +106,21 @@ namespace osu.Game.Overlays.Mods
private ShearedToggleButton? customisationButton;
+ private Sample? columnAppearSample;
+
protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
{
}
[BackgroundDependencyLoader]
- private void load(OsuGameBase game, OsuColour colours)
+ private void load(OsuGameBase game, OsuColour colours, AudioManager audio)
{
Header.Title = ModSelectOverlayStrings.ModSelectTitle;
Header.Description = ModSelectOverlayStrings.ModSelectDescription;
+ columnAppearSample = audio.Samples.Get(@"SongSelect/mod-column-pop-in");
+
AddRange(new Drawable[]
{
new ClickToReturnContainer
@@ -453,8 +462,31 @@ namespace osu.Game.Overlays.Mods
.MoveToY(0, duration, Easing.OutQuint)
.FadeIn(duration, Easing.OutQuint);
- if (!allFiltered)
- nonFilteredColumnCount += 1;
+ if (allFiltered)
+ continue;
+
+ int columnNumber = nonFilteredColumnCount;
+ Scheduler.AddDelayed(() =>
+ {
+ var channel = columnAppearSample?.GetChannel();
+ if (channel == null) return;
+
+ // Still play sound effects for off-screen columns up to a certain point.
+ if (columnNumber > 5 && !column.Active.Value) return;
+
+ // use X position of the column on screen as a basis for panning the sample
+ float balance = column.Parent.BoundingBox.Centre.X / RelativeToAbsoluteFactor.X;
+
+ // dip frequency and ramp volume of sample over the first 5 displayed columns
+ float progress = Math.Min(1, columnNumber / 5f);
+
+ channel.Frequency.Value = 1.3 - (progress * 0.3) + RNG.NextDouble(0.1);
+ channel.Volume.Value = Math.Max(progress, 0.2);
+ channel.Balance.Value = -1 + balance * 2;
+ channel.Play();
+ }, delay);
+
+ nonFilteredColumnCount += 1;
}
}
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index bce7e9b797..8af295dfe8 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -14,6 +14,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -106,10 +107,12 @@ namespace osu.Game.Overlays
if (beatmap.Disabled)
return;
+ Logger.Log($"{nameof(MusicController)} skipping next track to {nameof(EnsurePlayingSomething)}");
NextTrack();
}
else if (!IsPlaying)
{
+ Logger.Log($"{nameof(MusicController)} starting playback to {nameof(EnsurePlayingSomething)}");
Play();
}
}
@@ -130,9 +133,9 @@ namespace osu.Game.Overlays
UserPauseRequested = false;
if (restart)
- CurrentTrack.Restart();
+ CurrentTrack.RestartAsync();
else if (!IsPlaying)
- CurrentTrack.Start();
+ CurrentTrack.StartAsync();
return true;
}
@@ -149,7 +152,7 @@ namespace osu.Game.Overlays
{
UserPauseRequested |= requestedByUser;
if (CurrentTrack.IsRunning)
- CurrentTrack.Stop();
+ CurrentTrack.StopAsync();
}
///
@@ -247,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/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index 54a262f6a6..90a357a281 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
Spacing = new Vector2(2),
Children = Score.Mods.Select(mod =>
{
- var ruleset = rulesets.GetRuleset(Score.RulesetID) ?? throw new InvalidOperationException();
+ var ruleset = rulesets.GetRuleset(Score.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {Score.RulesetID} not found locally");
return new ModIcon(ruleset.CreateInstance().CreateModFromAcronym(mod.Acronym))
{
diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
index 77eede0e46..42ac4adb34 100644
--- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
Action = () =>
{
// Blocking operations implicitly causes a Compact().
- using (realm.BlockAllOperations())
+ using (realm.BlockAllOperations("compact"))
{
}
}
@@ -58,7 +58,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
{
try
{
- var token = realm.BlockAllOperations();
+ var token = realm.BlockAllOperations("maintenance");
blockAction.Enabled.Value = false;
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/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs
index 507e116723..708bee6fbd 100644
--- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs
@@ -3,13 +3,10 @@
#nullable disable
-using System;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.Mods.Input;
@@ -17,20 +14,11 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
public class SongSelectSettings : SettingsSubsection
{
- private Bindable minStars;
- private Bindable maxStars;
-
protected override LocalisableString Header => UserInterfaceStrings.SongSelectHeader;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
- minStars = config.GetBindable(OsuSetting.DisplayStarsMinimum);
- maxStars = config.GetBindable(OsuSetting.DisplayStarsMaximum);
-
- minStars.ValueChanged += min => maxStars.Value = Math.Max(min.NewValue, maxStars.Value);
- maxStars.ValueChanged += max => minStars.Value = Math.Min(max.NewValue, minStars.Value);
-
Children = new Drawable[]
{
new SettingsCheckbox
@@ -44,20 +32,6 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelText = UserInterfaceStrings.ShowConvertedBeatmaps,
Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps),
},
- new SettingsSlider
- {
- LabelText = UserInterfaceStrings.StarsMinimum,
- Current = config.GetBindable(OsuSetting.DisplayStarsMinimum),
- KeyboardStep = 0.1f,
- Keywords = new[] { "minimum", "maximum", "star", "difficulty" }
- },
- new SettingsSlider
- {
- LabelText = UserInterfaceStrings.StarsMaximum,
- Current = config.GetBindable(OsuSetting.DisplayStarsMaximum),
- KeyboardStep = 0.1f,
- Keywords = new[] { "minimum", "maximum", "star", "difficulty" }
- },
new SettingsEnumDropdown
{
LabelText = UserInterfaceStrings.RandomSelectionAlgorithm,
@@ -71,15 +45,5 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
}
};
}
-
- private class MaximumStarsSlider : StarsSlider
- {
- public override LocalisableString TooltipText => Current.IsDefault ? UserInterfaceStrings.NoLimit : base.TooltipText;
- }
-
- private class StarsSlider : OsuSliderBar
- {
- public override LocalisableString TooltipText => Current.Value.ToString(@"0.## stars");
- }
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs
index 2f182d537f..db0dc8fd5e 100644
--- a/osu.Game/Overlays/Settings/SettingsFooter.cs
+++ b/osu.Game/Overlays/Settings/SettingsFooter.cs
@@ -3,11 +3,11 @@
#nullable disable
-using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Logging;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -28,32 +28,17 @@ namespace osu.Game.Overlays.Settings
Direction = FillDirection.Vertical;
Padding = new MarginPadding { Top = 20, Bottom = 30, Horizontal = SettingsPanel.CONTENT_MARGINS };
- var modes = new List();
-
- foreach (var ruleset in rulesets.AvailableRulesets)
- {
- var icon = new ConstrainedIconContainer
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Icon = ruleset.CreateInstance().CreateIcon(),
- Colour = Color4.Gray,
- Size = new Vector2(20),
- };
-
- modes.Add(icon);
- }
+ FillFlowContainer modes;
Children = new Drawable[]
{
- new FillFlowContainer
+ modes = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Direction = FillDirection.Full,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Children = modes,
Spacing = new Vector2(5),
Padding = new MarginPadding { Bottom = 10 },
},
@@ -70,6 +55,27 @@ namespace osu.Game.Overlays.Settings
Origin = Anchor.TopCentre,
}
};
+
+ foreach (var ruleset in rulesets.AvailableRulesets)
+ {
+ try
+ {
+ var icon = new ConstrainedIconContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Icon = ruleset.CreateInstance().CreateIcon(),
+ Colour = Color4.Gray,
+ Size = new Vector2(20),
+ };
+
+ modes.Add(icon);
+ }
+ catch
+ {
+ Logger.Log($"Could not create ruleset icon for {ruleset.Name}. Please check for an update from the developer.", level: LogLevel.Error);
+ }
+ }
}
private class BuildDisplay : OsuAnimatedButton
diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs
index 84a5bf1144..caec4aeed0 100644
--- a/osu.Game/Overlays/TabControlOverlayHeader.cs
+++ b/osu.Game/Overlays/TabControlOverlayHeader.cs
@@ -3,11 +3,11 @@
#nullable disable
-using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -108,19 +108,7 @@ namespace osu.Game.Overlays
public OverlayHeaderTabItem(T value)
: base(value)
{
- if (!(Value is Enum enumValue))
- Text.Text = Value.ToString().ToLowerInvariant();
- else
- {
- var localisableDescription = enumValue.GetLocalisableDescription();
- string nonLocalisableDescription = enumValue.GetDescription();
-
- // If localisable == non-localisable, then we must have a basic string, so .ToLowerInvariant() is used.
- Text.Text = localisableDescription.Equals(nonLocalisableDescription)
- ? nonLocalisableDescription.ToLowerInvariant()
- : localisableDescription;
- }
-
+ Text.Text = value.GetLocalisableDescription().ToLower();
Text.Font = OsuFont.GetFont(size: 14);
Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
Bar.Margin = new MarginPadding { Bottom = bar_height };
diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
index d0574d65ab..f6abf259e8 100644
--- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
+++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.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 MessagePack;
using Newtonsoft.Json;
using osu.Framework.Extensions.EnumExtensions;
diff --git a/osu.Game/Replays/Legacy/ReplayButtonState.cs b/osu.Game/Replays/Legacy/ReplayButtonState.cs
index 7788918ba9..4b02cf2cd5 100644
--- a/osu.Game/Replays/Legacy/ReplayButtonState.cs
+++ b/osu.Game/Replays/Legacy/ReplayButtonState.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;
namespace osu.Game.Replays.Legacy
diff --git a/osu.Game/Replays/Replay.cs b/osu.Game/Replays/Replay.cs
index 4903f8c47f..30e176b5c7 100644
--- a/osu.Game/Replays/Replay.cs
+++ b/osu.Game/Replays/Replay.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 osu.Game.Rulesets.Replays;
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
index e0844a5c8a..bd45482235 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
@@ -1,11 +1,11 @@
// 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;
using Newtonsoft.Json;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty
@@ -26,11 +26,12 @@ namespace osu.Game.Rulesets.Difficulty
protected const int ATTRIB_ID_SCORE_MULTIPLIER = 15;
protected const int ATTRIB_ID_FLASHLIGHT = 17;
protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
+ protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
///
/// The mods which were applied to the beatmap.
///
- public Mod[] Mods { get; set; }
+ public Mod[] Mods { get; set; } = Array.Empty();
///
/// The combined star rating of all skills.
@@ -74,7 +75,8 @@ namespace osu.Game.Rulesets.Difficulty
/// Reads osu-web database attribute mappings into this object.
///
/// The attribute mappings.
- public virtual void FromDatabaseAttributes(IReadOnlyDictionary values)
+ /// The where more information about the beatmap may be extracted from (such as AR/CS/OD/etc).
+ public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo)
{
}
}
diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
index fa44f81df3..dd2ad2cbfa 100644
--- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
+++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.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.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
index a42d4a049a..599e81f2b8 100644
--- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
+++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods
@@ -103,9 +104,9 @@ namespace osu.Game.Rulesets.Mods
{
InternalChildren = new Drawable[]
{
- new SettingsSlider
+ new OsuSliderBar
{
- ShowsDefaultIndicator = false,
+ RelativeSizeAxes = Axes.X,
Current = currentNumber,
KeyboardStep = 0.1f,
}
diff --git a/osu.Game/Rulesets/Replays/AutoGenerator.cs b/osu.Game/Rulesets/Replays/AutoGenerator.cs
index 7fab84eba8..f4b96b3884 100644
--- a/osu.Game/Rulesets/Replays/AutoGenerator.cs
+++ b/osu.Game/Rulesets/Replays/AutoGenerator.cs
@@ -1,11 +1,8 @@
// 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 JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects;
@@ -34,7 +31,7 @@ namespace osu.Game.Rulesets.Replays
///
public abstract Replay Generate();
- protected virtual HitObject GetNextObject(int currentIndex)
+ protected virtual HitObject? GetNextObject(int currentIndex)
{
if (currentIndex >= Beatmap.HitObjects.Count - 1)
return null;
@@ -51,8 +48,7 @@ namespace osu.Game.Rulesets.Replays
///
protected readonly List Frames = new List();
- [CanBeNull]
- protected TFrame LastFrame => Frames.Count == 0 ? null : Frames[^1];
+ protected TFrame? LastFrame => Frames.Count == 0 ? null : Frames[^1];
protected AutoGenerator(IBeatmap beatmap)
: base(beatmap)
diff --git a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs
index 6753a23bca..9a4af9e4ee 100644
--- a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs
+++ b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.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.Beatmaps;
using osu.Game.Replays.Legacy;
@@ -19,7 +17,7 @@ namespace osu.Game.Rulesets.Replays.Types
/// The to extract values from.
/// The beatmap.
/// The last post-conversion , used to fill in missing delta information. May be null.
- void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null);
+ void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null);
///
/// Populates this using values from a .
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index e098472999..c1ec6c30ef 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets
break;
case ModPerfect:
- value |= LegacyMods.Perfect;
+ value |= LegacyMods.Perfect | LegacyMods.SuddenDeath;
break;
case ModSuddenDeath:
@@ -151,7 +151,7 @@ namespace osu.Game.Rulesets
break;
case ModNightcore:
- value |= LegacyMods.Nightcore;
+ value |= LegacyMods.Nightcore | LegacyMods.DoubleTime;
break;
case ModDoubleTime:
@@ -171,7 +171,7 @@ namespace osu.Game.Rulesets
break;
case ModCinema:
- value |= LegacyMods.Cinema;
+ value |= LegacyMods.Cinema | LegacyMods.Autoplay;
break;
case ModAutoplay:
diff --git a/osu.Game/Rulesets/RulesetSelector.cs b/osu.Game/Rulesets/RulesetSelector.cs
index 35244eb86e..701e60eec9 100644
--- a/osu.Game/Rulesets/RulesetSelector.cs
+++ b/osu.Game/Rulesets/RulesetSelector.cs
@@ -5,6 +5,7 @@
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Allocation;
+using osu.Framework.Logging;
namespace osu.Game.Rulesets
{
@@ -18,8 +19,17 @@ namespace osu.Game.Rulesets
[BackgroundDependencyLoader]
private void load()
{
- foreach (var r in Rulesets.AvailableRulesets)
- AddItem(r);
+ foreach (var ruleset in Rulesets.AvailableRulesets)
+ {
+ try
+ {
+ AddItem(ruleset);
+ }
+ catch
+ {
+ Logger.Log($"Could not create ruleset icon for {ruleset.Name}. Please check for an update from the developer.", level: LogLevel.Error);
+ }
+ }
}
}
}
diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs
index 0f21a497b0..b1f355a789 100644
--- a/osu.Game/Rulesets/UI/ModIcon.cs
+++ b/osu.Game/Rulesets/UI/ModIcon.cs
@@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osuTK;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.UI
@@ -53,7 +54,6 @@ namespace osu.Game.Rulesets.UI
private OsuColour colours { get; set; }
private Color4 backgroundColour;
- private Color4 highlightedColour;
///
/// Construct a new instance.
@@ -123,47 +123,13 @@ namespace osu.Game.Rulesets.UI
modAcronym.FadeOut();
}
- switch (value.Type)
- {
- default:
- case ModType.DifficultyIncrease:
- backgroundColour = colours.Yellow;
- highlightedColour = colours.YellowLight;
- break;
-
- case ModType.DifficultyReduction:
- backgroundColour = colours.Green;
- highlightedColour = colours.GreenLight;
- break;
-
- case ModType.Automation:
- backgroundColour = colours.Blue;
- highlightedColour = colours.BlueLight;
- break;
-
- case ModType.Conversion:
- backgroundColour = colours.Purple;
- highlightedColour = colours.PurpleLight;
- break;
-
- case ModType.Fun:
- backgroundColour = colours.Pink;
- highlightedColour = colours.PinkLight;
- break;
-
- case ModType.System:
- backgroundColour = colours.Gray6;
- highlightedColour = colours.Gray7;
- modIcon.Colour = colours.Yellow;
- break;
- }
-
+ backgroundColour = colours.ForModType(value.Type);
updateColour();
}
private void updateColour()
{
- background.Colour = Selected.Value ? highlightedColour : backgroundColour;
+ background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour;
}
}
}
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
index 85cf463a13..750bb50be3 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@@ -143,6 +144,7 @@ namespace osu.Game.Scoring.Legacy
return legacyFrame;
case IConvertibleReplayFrame convertibleFrame:
+ Debug.Assert(beatmap != null);
return convertibleFrame.ToLegacy(beatmap);
default:
diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs
index f59ffc7c94..53dd511d57 100644
--- a/osu.Game/Scoring/ScoreImporter.cs
+++ b/osu.Game/Scoring/ScoreImporter.cs
@@ -13,6 +13,9 @@ using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Rulesets;
using osu.Game.Scoring.Legacy;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
using Realms;
namespace osu.Game.Scoring
@@ -26,11 +29,14 @@ namespace osu.Game.Scoring
private readonly RulesetStore rulesets;
private readonly Func beatmaps;
- public ScoreImporter(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm)
+ private readonly IAPIProvider api;
+
+ public ScoreImporter(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, IAPIProvider api)
: base(storage, realm)
{
this.rulesets = rulesets;
this.beatmaps = beatmaps;
+ this.api = api;
}
protected override ScoreInfo? CreateModel(ArchiveReader archive)
@@ -68,5 +74,17 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(model.StatisticsJson))
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
}
+
+ protected override void PostImport(ScoreInfo model, Realm realm)
+ {
+ base.PostImport(model, realm);
+
+ var userRequest = new GetUserRequest(model.RealmUser.Username);
+
+ api.Perform(userRequest);
+
+ if (userRequest.Response is APIUser user)
+ model.RealmUser.OnlineID = user.Id;
+ }
}
}
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 6ee1d11f83..9aed8904e6 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -21,6 +21,7 @@ using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Online.API;
namespace osu.Game.Scoring
{
@@ -31,7 +32,7 @@ namespace osu.Game.Scoring
private readonly OsuConfigManager configManager;
private readonly ScoreImporter scoreImporter;
- public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler,
+ public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, IAPIProvider api,
BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null)
: base(storage, realm)
{
@@ -39,7 +40,7 @@ namespace osu.Game.Scoring
this.difficultyCache = difficultyCache;
this.configManager = configManager;
- scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm)
+ scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api)
{
PostNotification = obj => PostNotification?.Invoke(obj)
};
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/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 20bdf75b7d..48576b81e2 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -21,6 +21,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
+using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -39,6 +40,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
@@ -97,6 +99,30 @@ namespace osu.Game.Screens.Edit
public IBindable SamplePlaybackDisabled => samplePlaybackDisabled;
+ ///
+ /// Ensure all asynchronously loading pieces of the editor are in a good state.
+ /// This exists here for convenience for tests, not for actual use.
+ /// Eventually we'd probably want a better way to signal this.
+ ///
+ public bool ReadyForUse
+ {
+ get
+ {
+ if (!workingBeatmapUpdated)
+ return false;
+
+ if (currentScreen?.IsLoaded != true)
+ return false;
+
+ if (currentScreen is EditorScreenWithTimeline)
+ return currentScreen.ChildrenOfType().FirstOrDefault()?.IsLoaded == true;
+
+ return true;
+ }
+ }
+
+ private bool workingBeatmapUpdated;
+
private readonly Bindable samplePlaybackDisabled = new Bindable();
private bool canSave;
@@ -160,7 +186,7 @@ namespace osu.Game.Screens.Edit
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
// required so we can get the track length in EditorClock.
- // this is safe as nothing has yet got a reference to this new beatmap.
+ // this is ONLY safe because the track being provided is a `TrackVirtual` which we don't really care about disposing.
loadableBeatmap.LoadTrack();
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
@@ -219,6 +245,7 @@ namespace osu.Game.Screens.Edit
// this assumes that nothing during the rest of this load() method is accessing Beatmap.Value (loadableBeatmap should be preferred).
// generally this is quite safe, as the actual load of editor content comes after menuBar.Mode.ValueChanged is fired in its own LoadComplete.
Beatmap.Value = loadableBeatmap;
+ workingBeatmapUpdated = true;
});
OsuMenuItem undoMenuItem;
@@ -630,9 +657,7 @@ namespace osu.Game.Screens.Edit
// To update the game-wide beatmap with any changes, perform a re-fetch on exit/suspend.
// This is required as the editor makes its local changes via EditorBeatmap
// (which are not propagated outwards to a potentially cached WorkingBeatmap).
- ((IWorkingBeatmapCache)beatmapManager).Invalidate(Beatmap.Value.BeatmapInfo);
- var refetchedBeatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == Beatmap.Value.BeatmapInfo.ID);
- var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(refetchedBeatmapInfo);
+ var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true);
if (!(refetchedBeatmap is DummyWorkingBeatmap))
{
@@ -896,7 +921,7 @@ namespace osu.Game.Screens.Edit
private void cancelExit()
{
- samplePlaybackDisabled.Value = false;
+ updateSampleDisabledState();
loader?.CancelPendingDifficultySwitch();
}
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 0a6f9974b0..96425e8bc8 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -122,7 +122,7 @@ namespace osu.Game.Screens.Edit
public BeatmapInfo BeatmapInfo
{
get => beatmapInfo;
- set => throw new InvalidOperationException();
+ set => throw new InvalidOperationException($"Can't set {nameof(BeatmapInfo)} on {nameof(EditorBeatmap)}");
}
public BeatmapMetadata Metadata => beatmapInfo.Metadata;
diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
index 87e640badc..fd230a97bc 100644
--- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
+++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
@@ -35,7 +35,13 @@ namespace osu.Game.Screens.Edit.GameplayTest
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
{
if (completed.NewValue)
- Scheduler.AddDelayed(this.Exit, RESULTS_DISPLAY_DELAY);
+ {
+ Scheduler.AddDelayed(() =>
+ {
+ if (this.IsCurrentScreen())
+ this.Exit();
+ }, RESULTS_DISPLAY_DELAY);
+ }
});
}
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 74be02728f..04bffda81b 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -191,20 +191,48 @@ namespace osu.Game.Screens.Menu
State = ButtonSystemState.Initial;
}
- protected override bool OnKeyDown(KeyDownEvent e)
+ ///
+ /// Triggers the if the current is .
+ ///
+ /// true if the was triggered, false otherwise.
+ private bool triggerInitialOsuLogo()
{
- if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
- return false;
-
if (State == ButtonSystemState.Initial)
{
logo?.TriggerClick();
return true;
}
+ return false;
+ }
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
+ return false;
+
+ if (triggerInitialOsuLogo())
+ return true;
+
return base.OnKeyDown(e);
}
+ protected override bool OnJoystickPress(JoystickPressEvent e)
+ {
+ if (triggerInitialOsuLogo())
+ return true;
+
+ return base.OnJoystickPress(e);
+ }
+
+ protected override bool OnMidiDown(MidiDownEvent e)
+ {
+ if (triggerInitialOsuLogo())
+ return true;
+
+ return base.OnMidiDown(e);
+ }
+
public bool OnPressed(KeyBindingPressEvent e)
{
if (e.Repeat)
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 d59a07a350..6ad0350e43 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -4,23 +4,22 @@
#nullable disable
using System;
-using System.Collections.Generic;
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.Utils;
+using osu.Framework.Logging;
+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,14 +67,22 @@ 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,
LoadMenu = LoadMenu
- }, t =>
+ }, _ =>
{
- AddInternal(t);
+ AddInternal(intro);
+
+ // There is a chance that the intro timed out before being displayed, and this scheduled callback could
+ // happen during the outro rather than intro.
+ // In such a scenario, we don't want to play the intro sample, nor attempt to start the intro track
+ // (that may have already been since disposed by MusicController).
+ if (DidLoadMenu)
+ return;
+
if (!UsingThemedIntro)
welcome?.Play();
@@ -95,19 +95,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 +107,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 +119,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 +196,6 @@ namespace osu.Game.Screens.Menu
rulesets.Hide();
lazerLogo.Hide();
- background.ApplyToBackground(b => b.Hide());
using (BeginAbsoluteSequence(0))
{
@@ -267,7 +257,7 @@ namespace osu.Game.Screens.Menu
logo.FadeIn();
- background.ApplyToBackground(b => b.Show());
+ showBackgroundAction();
game.Add(new GameWideFlash());
@@ -340,24 +330,28 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- var modes = new List();
-
- foreach (var ruleset in rulesets.AvailableRulesets)
- {
- var icon = new ConstrainedIconContainer
- {
- Icon = ruleset.CreateInstance().CreateIcon(),
- Size = new Vector2(30),
- };
-
- modes.Add(icon);
- }
-
AutoSizeAxes = Axes.Both;
- Children = modes;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
+
+ foreach (var ruleset in rulesets.AvailableRulesets)
+ {
+ try
+ {
+ var icon = new ConstrainedIconContainer
+ {
+ Icon = ruleset.CreateInstance().CreateIcon(),
+ Size = new Vector2(30),
+ };
+
+ Add(icon);
+ }
+ catch
+ {
+ Logger.Log($"Could not create ruleset icon for {ruleset.Name}. Please check for an update from the developer.", level: LogLevel.Error);
+ }
+ }
}
}
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 6a74a6bd6e..066a37055c 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
@@ -64,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;
@@ -147,7 +146,6 @@ namespace osu.Game.Screens.Menu
Buttons.OnSettings = () => settings?.ToggleVisibility();
Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
- LoadComponentAsync(background = new BackgroundScreenDefault());
preloadSongSelect();
}
@@ -302,6 +300,8 @@ namespace osu.Game.Screens.Menu
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
{
+ Logger.Log($"{nameof(MainMenu)} completing {nameof(PresentBeatmap)} with beatmap {beatmap} ruleset {ruleset}");
+
Beatmap.Value = beatmap;
Ruleset.Value = ruleset;
diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
index a3a176477e..f38077a9a7 100644
--- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
+++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
@@ -9,17 +9,20 @@ using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -27,6 +30,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
@@ -38,7 +42,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay
{
- public class DrawableRoomPlaylistItem : OsuRearrangeableListItem
+ public class DrawableRoomPlaylistItem : OsuRearrangeableListItem, IHasContextMenu
{
public const float HEIGHT = 50;
@@ -93,6 +97,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private RulesetStore rulesets { get; set; }
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
+
[Resolved]
private OsuColour colours { get; set; }
@@ -102,6 +109,15 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; }
+ [Resolved(CanBeNull = true)]
+ private BeatmapSetOverlay beatmapOverlay { get; set; }
+
+ [Resolved(CanBeNull = true)]
+ private CollectionManager collectionManager { get; set; }
+
+ [Resolved(CanBeNull = true)]
+ private ManageCollectionsDialog manageCollectionsDialog { get; set; }
+
protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model;
public DrawableRoomPlaylistItem(PlaylistItem item)
@@ -433,7 +449,7 @@ namespace osu.Game.Screens.OnlinePlay
}
}
},
- }
+ },
};
}
@@ -470,6 +486,31 @@ namespace osu.Game.Screens.OnlinePlay
return true;
}
+ public MenuItem[] ContextMenuItems
+ {
+ get
+ {
+ List
public class CleanRunHeadlessGameHost : TestRunHeadlessGameHost
{
+ private readonly bool bypassCleanupOnSetup;
+
///
/// Create a new instance.
///
/// Whether to bind IPC channels.
/// Whether the host should be forced to run in realtime, rather than accelerated test time.
- /// Whether to bypass directory cleanup on host disposal. Should be used only if a subsequent test relies on the files still existing.
+ /// Whether to bypass directory cleanup on .
+ /// Whether to bypass directory cleanup on host disposal. Should be used only if a subsequent test relies on the files still existing.
/// The name of the calling method, used for test file isolation and clean-up.
- public CleanRunHeadlessGameHost(bool bindIPC = false, bool realtime = true, bool bypassCleanup = false, [CallerMemberName] string callingMethodName = @"")
+ public CleanRunHeadlessGameHost(bool bindIPC = false, bool realtime = true, bool bypassCleanupOnSetup = false, bool bypassCleanupOnDispose = false,
+ [CallerMemberName] string callingMethodName = @"")
: base($"{callingMethodName}-{Guid.NewGuid()}", new HostOptions
{
BindIPC = bindIPC,
- }, bypassCleanup: bypassCleanup, realtime: realtime)
+ }, bypassCleanup: bypassCleanupOnDispose, realtime: realtime)
{
+ this.bypassCleanupOnSetup = bypassCleanupOnSetup;
}
protected override void SetupForRun()
{
- try
+ if (!bypassCleanupOnSetup)
{
- Storage.DeleteDirectory(string.Empty);
- }
- catch
- {
- // May fail if a logging target has already been set via OsuStorage.ChangeTargetStorage.
+ try
+ {
+ Storage.DeleteDirectory(string.Empty);
+ }
+ catch
+ {
+ // May fail if a logging target has already been set via OsuStorage.ChangeTargetStorage.
+ }
}
// base call needs to be run *after* storage is emptied, as it updates the (static) logger's storage and may start writing
diff --git a/osu.Game/Tests/FlakyTestAttribute.cs b/osu.Game/Tests/FlakyTestAttribute.cs
new file mode 100644
index 0000000000..c61ce80bf5
--- /dev/null
+++ b/osu.Game/Tests/FlakyTestAttribute.cs
@@ -0,0 +1,25 @@
+// 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 NUnit.Framework;
+
+namespace osu.Game.Tests
+{
+ ///
+ /// An attribute to mark any flaky tests.
+ /// Will add a retry count unless environment variable `FAIL_FLAKY_TESTS` is set to `1`.
+ ///
+ public class FlakyTestAttribute : RetryAttribute
+ {
+ public FlakyTestAttribute()
+ : this(10)
+ {
+ }
+
+ public FlakyTestAttribute(int tryCount)
+ : base(Environment.GetEnvironmentVariable("OSU_TESTS_FAIL_FLAKY") == "1" ? 1 : tryCount)
+ {
+ }
+ }
+}
diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs
index 3f5fee5768..31036247ab 100644
--- a/osu.Game/Tests/Visual/EditorTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorTestScene.cs
@@ -17,9 +17,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
-using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Menu;
using osu.Game.Skinning;
@@ -58,15 +56,13 @@ namespace osu.Game.Tests.Visual
Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
}
- protected virtual bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true
- && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true;
-
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("load editor", LoadEditor);
- AddUntilStep("wait for editor to load", () => EditorComponentsReady);
+ AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true);
+ AddUntilStep("wait for beatmap updated", () => !Beatmap.IsDefault);
}
protected virtual void LoadEditor()
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 6b7495762a..19b887eea5 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -6,9 +6,11 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
+using MessagePack;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
@@ -29,7 +31,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
///
/// The local client's . This is not always equivalent to the server-side room.
///
- public new Room? APIRoom => base.APIRoom;
+ public Room? ClientAPIRoom => base.APIRoom;
+
+ ///
+ /// The local client's . This is not always equivalent to the server-side room.
+ ///
+ public MultiplayerRoom? ClientRoom => base.Room;
+
+ ///
+ /// The server's . This is always up-to-date.
+ ///
+ public Room? ServerAPIRoom { get; private set; }
+
+ ///
+ /// The server's . This is always up-to-date.
+ ///
+ public MultiplayerRoom? ServerRoom { get; private set; }
+
+ [Obsolete]
+ protected new Room APIRoom => throw new InvalidOperationException($"Accessing the client-side API room via {nameof(TestMultiplayerClient)} is unsafe. "
+ + $"Use {nameof(ClientAPIRoom)} if this was intended.");
+
+ [Obsolete]
+ public new MultiplayerRoom Room => throw new InvalidOperationException($"Accessing the client-side room via {nameof(TestMultiplayerClient)} is unsafe. "
+ + $"Use {nameof(ClientRoom)} if this was intended.");
+
+ public new MultiplayerRoomUser? LocalUser => ServerRoom?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
public Action? RoomSetupAction;
@@ -40,17 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private readonly TestMultiplayerRoomManager roomManager;
- ///
- /// Guaranteed up-to-date playlist.
- ///
- private readonly List serverSidePlaylist = new List();
-
- ///
- /// Guaranteed up-to-date API room.
- ///
- private Room? serverSideAPIRoom;
-
- private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex];
+ private MultiplayerPlaylistItem? currentItem => ServerRoom?.Playlist[currentIndex];
private int currentIndex;
private long lastPlaylistItemId;
@@ -79,153 +96,163 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void addUser(MultiplayerRoomUser user)
{
- ((IMultiplayerClient)this).UserJoined(user).WaitSafely();
+ Debug.Assert(ServerRoom != null);
- // We want the user to be immediately available for testing, so force a scheduler update to run the update-bound continuation.
- Scheduler.Update();
+ ServerRoom.Users.Add(user);
+ ((IMultiplayerClient)this).UserJoined(clone(user)).WaitSafely();
- switch (Room?.MatchState)
+ switch (ServerRoom?.MatchState)
{
case TeamVersusRoomState teamVersus:
// simulate the server's automatic assignment of users to teams on join.
// the "best" team is the one with the least users on it.
int bestTeam = teamVersus.Teams
- .Select(team => (teamID: team.ID, userCount: Room.Users.Count(u => (u.MatchState as TeamVersusUserState)?.TeamID == team.ID)))
+ .Select(team => (teamID: team.ID, userCount: ServerRoom.Users.Count(u => (u.MatchState as TeamVersusUserState)?.TeamID == team.ID)))
.OrderBy(pair => pair.userCount)
.First().teamID;
- ((IMultiplayerClient)this).MatchUserStateChanged(user.UserID, new TeamVersusUserState { TeamID = bestTeam }).WaitSafely();
+
+ user.MatchState = new TeamVersusUserState { TeamID = bestTeam };
+ ((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).WaitSafely();
break;
}
}
public void RemoveUser(APIUser user)
{
- Debug.Assert(Room != null);
+ Debug.Assert(ServerRoom != null);
- ((IMultiplayerClient)this).UserLeft(new MultiplayerRoomUser(user.Id));
+ ServerRoom.Users.Remove(ServerRoom.Users.Single(u => u.UserID == user.Id));
+ ((IMultiplayerClient)this).UserLeft(clone(new MultiplayerRoomUser(user.Id)));
- Schedule(() =>
- {
- if (Room.Users.Any())
- TransferHost(Room.Users.First().UserID);
- });
+ if (ServerRoom.Users.Any())
+ TransferHost(ServerRoom.Users.First().UserID);
}
public void ChangeRoomState(MultiplayerRoomState newState)
{
- ((IMultiplayerClient)this).RoomStateChanged(newState);
+ Debug.Assert(ServerRoom != null);
+
+ ServerRoom.State = clone(newState);
+
+ ((IMultiplayerClient)this).RoomStateChanged(clone(ServerRoom.State));
}
public void ChangeUserState(int userId, MultiplayerUserState newState)
{
- ((IMultiplayerClient)this).UserStateChanged(userId, newState);
+ Debug.Assert(ServerRoom != null);
+
+ var user = ServerRoom.Users.Single(u => u.UserID == userId);
+ user.State = clone(newState);
+
+ ((IMultiplayerClient)this).UserStateChanged(clone(userId), clone(user.State));
+
updateRoomStateIfRequired();
}
private void updateRoomStateIfRequired()
{
- Debug.Assert(APIRoom != null);
+ Debug.Assert(ServerRoom != null);
- Schedule(() =>
+ switch (ServerRoom.State)
{
- Debug.Assert(Room != null);
+ case MultiplayerRoomState.Open:
+ break;
- switch (Room.State)
- {
- case MultiplayerRoomState.Open:
- break;
+ case MultiplayerRoomState.WaitingForLoad:
+ if (ServerRoom.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
+ {
+ var loadedUsers = ServerRoom.Users.Where(u => u.State == MultiplayerUserState.Loaded).ToArray();
- case MultiplayerRoomState.WaitingForLoad:
- if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
+ if (loadedUsers.Length == 0)
{
- var loadedUsers = Room.Users.Where(u => u.State == MultiplayerUserState.Loaded).ToArray();
-
- if (loadedUsers.Length == 0)
- {
- // all users have bailed from the load sequence. cancel the game start.
- ChangeRoomState(MultiplayerRoomState.Open);
- return;
- }
-
- foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
- ChangeUserState(u.UserID, MultiplayerUserState.Playing);
-
- ((IMultiplayerClient)this).GameplayStarted();
-
- ChangeRoomState(MultiplayerRoomState.Playing);
- }
-
- break;
-
- case MultiplayerRoomState.Playing:
- if (Room.Users.All(u => u.State != MultiplayerUserState.Playing))
- {
- foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
- ChangeUserState(u.UserID, MultiplayerUserState.Results);
-
+ // all users have bailed from the load sequence. cancel the game start.
ChangeRoomState(MultiplayerRoomState.Open);
- ((IMultiplayerClient)this).ResultsReady();
-
- FinishCurrentItem().WaitSafely();
+ return;
}
- break;
- }
- });
+ foreach (var u in ServerRoom.Users.Where(u => u.State == MultiplayerUserState.Loaded))
+ ChangeUserState(u.UserID, MultiplayerUserState.Playing);
+
+ ((IMultiplayerClient)this).GameplayStarted();
+
+ ChangeRoomState(MultiplayerRoomState.Playing);
+ }
+
+ break;
+
+ case MultiplayerRoomState.Playing:
+ if (ServerRoom.Users.All(u => u.State != MultiplayerUserState.Playing))
+ {
+ foreach (var u in ServerRoom.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
+ ChangeUserState(u.UserID, MultiplayerUserState.Results);
+
+ ChangeRoomState(MultiplayerRoomState.Open);
+ ((IMultiplayerClient)this).ResultsReady();
+
+ FinishCurrentItem().WaitSafely();
+ }
+
+ break;
+ }
}
public void ChangeUserBeatmapAvailability(int userId, BeatmapAvailability newBeatmapAvailability)
{
- ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(userId, newBeatmapAvailability);
+ Debug.Assert(ServerRoom != null);
+
+ var user = ServerRoom.Users.Single(u => u.UserID == userId);
+ user.BeatmapAvailability = newBeatmapAvailability;
+
+ ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(clone(userId), clone(user.BeatmapAvailability));
}
protected override async Task JoinRoom(long roomId, string? password = null)
{
- serverSideAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId);
+ roomId = clone(roomId);
+ password = clone(password);
- if (password != serverSideAPIRoom.Password.Value)
+ ServerAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId);
+
+ if (password != ServerAPIRoom.Password.Value)
throw new InvalidOperationException("Invalid password.");
- serverSidePlaylist.Clear();
- serverSidePlaylist.AddRange(serverSideAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)));
- lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID);
+ lastPlaylistItemId = ServerAPIRoom.Playlist.Max(item => item.ID);
var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
{
User = api.LocalUser.Value
};
- var room = new MultiplayerRoom(roomId)
+ ServerRoom = new MultiplayerRoom(roomId)
{
Settings =
{
- Name = serverSideAPIRoom.Name.Value,
- MatchType = serverSideAPIRoom.Type.Value,
+ Name = ServerAPIRoom.Name.Value,
+ MatchType = ServerAPIRoom.Type.Value,
Password = password,
- QueueMode = serverSideAPIRoom.QueueMode.Value,
- AutoStartDuration = serverSideAPIRoom.AutoStartDuration.Value
+ QueueMode = ServerAPIRoom.QueueMode.Value,
+ AutoStartDuration = ServerAPIRoom.AutoStartDuration.Value
},
- Playlist = serverSidePlaylist.ToList(),
+ Playlist = ServerAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)).ToList(),
Users = { localUser },
Host = localUser
};
- await updatePlaylistOrder(room).ConfigureAwait(false);
- await updateCurrentItem(room, false).ConfigureAwait(false);
+ await updatePlaylistOrder(ServerRoom).ConfigureAwait(false);
+ await updateCurrentItem(ServerRoom, false).ConfigureAwait(false);
- RoomSetupAction?.Invoke(room);
+ RoomSetupAction?.Invoke(ServerRoom);
RoomSetupAction = null;
- return room;
+ return clone(ServerRoom);
}
protected override void OnRoomJoined()
{
- Debug.Assert(APIRoom != null);
- Debug.Assert(Room != null);
+ Debug.Assert(ServerRoom != null);
// emulate the server sending this after the join room. scheduler required to make sure the join room event is fired first (in Join).
- changeMatchType(Room.Settings.MatchType).WaitSafely();
+ changeMatchType(ServerRoom.Settings.MatchType).WaitSafely();
RoomJoined = true;
}
@@ -236,29 +263,45 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask;
}
- public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);
+ public override Task TransferHost(int userId)
+ {
+ userId = clone(userId);
+
+ Debug.Assert(ServerRoom != null);
+
+ ServerRoom.Host = ServerRoom.Users.Single(u => u.UserID == userId);
+
+ return ((IMultiplayerClient)this).HostChanged(clone(userId));
+ }
public override Task KickUser(int userId)
{
- Debug.Assert(Room != null);
+ userId = clone(userId);
- return ((IMultiplayerClient)this).UserKicked(Room.Users.Single(u => u.UserID == userId));
+ Debug.Assert(ServerRoom != null);
+
+ var user = ServerRoom.Users.Single(u => u.UserID == userId);
+ ServerRoom.Users.Remove(user);
+
+ return ((IMultiplayerClient)this).UserKicked(clone(user));
}
public override async Task ChangeSettings(MultiplayerRoomSettings settings)
{
- Debug.Assert(Room != null);
- Debug.Assert(APIRoom != null);
+ settings = clone(settings);
+
+ Debug.Assert(ServerRoom != null);
Debug.Assert(currentItem != null);
// Server is authoritative for the time being.
- settings.PlaylistItemId = Room.Settings.PlaylistItemId;
+ settings.PlaylistItemId = ServerRoom.Settings.PlaylistItemId;
+ ServerRoom.Settings = settings;
await changeQueueMode(settings.QueueMode).ConfigureAwait(false);
- await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false);
+ await ((IMultiplayerClient)this).SettingsChanged(clone(settings)).ConfigureAwait(false);
- foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
+ foreach (var user in ServerRoom.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
await changeMatchType(settings.MatchType).ConfigureAwait(false);
@@ -267,43 +310,52 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task ChangeState(MultiplayerUserState newState)
{
+ newState = clone(newState);
+
if (newState == MultiplayerUserState.Idle && LocalUser?.State == MultiplayerUserState.WaitingForLoad)
return Task.CompletedTask;
- ChangeUserState(api.LocalUser.Value.Id, newState);
+ ChangeUserState(api.LocalUser.Value.Id, clone(newState));
return Task.CompletedTask;
}
public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability)
{
- ChangeUserBeatmapAvailability(api.LocalUser.Value.Id, newBeatmapAvailability);
+ ChangeUserBeatmapAvailability(api.LocalUser.Value.Id, clone(newBeatmapAvailability));
return Task.CompletedTask;
}
public void ChangeUserMods(int userId, IEnumerable newMods)
- => ChangeUserMods(userId, newMods.Select(m => new APIMod(m)).ToList());
+ => ChangeUserMods(userId, newMods.Select(m => new APIMod(m)));
public void ChangeUserMods(int userId, IEnumerable newMods)
{
- ((IMultiplayerClient)this).UserModsChanged(userId, newMods.ToList());
+ Debug.Assert(ServerRoom != null);
+
+ var user = ServerRoom.Users.Single(u => u.UserID == userId);
+ user.Mods = newMods.ToArray();
+
+ ((IMultiplayerClient)this).UserModsChanged(clone(userId), clone(user.Mods));
}
public override Task ChangeUserMods(IEnumerable newMods)
{
- ChangeUserMods(api.LocalUser.Value.Id, newMods);
+ ChangeUserMods(api.LocalUser.Value.Id, clone(newMods));
return Task.CompletedTask;
}
public override async Task SendMatchRequest(MatchUserRequest request)
{
- Debug.Assert(Room != null);
+ request = clone(request);
+
+ Debug.Assert(ServerRoom != null);
Debug.Assert(LocalUser != null);
switch (request)
{
case ChangeTeamRequest changeTeam:
- TeamVersusRoomState roomState = (TeamVersusRoomState)Room.MatchState!;
+ TeamVersusRoomState roomState = (TeamVersusRoomState)ServerRoom.MatchState!;
TeamVersusUserState userState = (TeamVersusUserState)LocalUser.MatchState!;
var targetTeam = roomState.Teams.FirstOrDefault(t => t.ID == changeTeam.TeamID);
@@ -312,7 +364,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
userState.TeamID = targetTeam.ID;
- await ((IMultiplayerClient)this).MatchUserStateChanged(LocalUser.UserID, userState).ConfigureAwait(false);
+ await ((IMultiplayerClient)this).MatchUserStateChanged(clone(LocalUser.UserID), clone(userState)).ConfigureAwait(false);
}
break;
@@ -321,10 +373,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task StartMatch()
{
- Debug.Assert(Room != null);
+ Debug.Assert(ServerRoom != null);
ChangeRoomState(MultiplayerRoomState.WaitingForLoad);
- foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
+ foreach (var user in ServerRoom.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
return ((IMultiplayerClient)this).LoadRequested();
@@ -341,36 +393,35 @@ namespace osu.Game.Tests.Visual.Multiplayer
public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
{
- Debug.Assert(Room != null);
- Debug.Assert(APIRoom != null);
+ Debug.Assert(ServerRoom != null);
Debug.Assert(currentItem != null);
- if (Room.Settings.QueueMode == QueueMode.HostOnly && Room.Host?.UserID != LocalUser?.UserID)
+ if (ServerRoom.Settings.QueueMode == QueueMode.HostOnly && ServerRoom.Host?.UserID != LocalUser?.UserID)
throw new InvalidOperationException("Local user is not the room host.");
item.OwnerID = userId;
await addItem(item).ConfigureAwait(false);
- await updateCurrentItem(Room).ConfigureAwait(false);
+ await updateCurrentItem(ServerRoom).ConfigureAwait(false);
updateRoomStateIfRequired();
}
- public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item);
+ public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(item));
public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
{
- Debug.Assert(Room != null);
+ Debug.Assert(ServerRoom != null);
Debug.Assert(currentItem != null);
- Debug.Assert(serverSideAPIRoom != null);
+ Debug.Assert(ServerAPIRoom != null);
item.OwnerID = userId;
- var existingItem = serverSidePlaylist.SingleOrDefault(i => i.ID == item.ID);
+ var existingItem = ServerRoom.Playlist.SingleOrDefault(i => i.ID == item.ID);
if (existingItem == null)
throw new InvalidOperationException("Attempted to change an item that doesn't exist.");
- if (existingItem.OwnerID != userId && Room.Host?.UserID != LocalUser?.UserID)
+ if (existingItem.OwnerID != userId && ServerRoom.Host?.UserID != LocalUser?.UserID)
throw new InvalidOperationException("Attempted to change an item which is not owned by the user.");
if (existingItem.Expired)
@@ -379,21 +430,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
// Ensure the playlist order doesn't change.
item.PlaylistOrder = existingItem.PlaylistOrder;
- serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item;
- serverSideAPIRoom.Playlist[serverSideAPIRoom.Playlist.IndexOf(serverSideAPIRoom.Playlist.Single(i => i.ID == item.ID))] = new PlaylistItem(item);
+ ServerRoom.Playlist[ServerRoom.Playlist.IndexOf(existingItem)] = item;
+ ServerAPIRoom.Playlist[ServerAPIRoom.Playlist.IndexOf(ServerAPIRoom.Playlist.Single(i => i.ID == item.ID))] = new PlaylistItem(item);
- await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
+ await ((IMultiplayerClient)this).PlaylistItemChanged(clone(item)).ConfigureAwait(false);
}
- public override Task EditPlaylistItem(MultiplayerPlaylistItem item) => EditUserPlaylistItem(api.LocalUser.Value.OnlineID, item);
+ public override Task EditPlaylistItem(MultiplayerPlaylistItem item) => EditUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(item));
public async Task RemoveUserPlaylistItem(int userId, long playlistItemId)
{
- Debug.Assert(Room != null);
- Debug.Assert(APIRoom != null);
- Debug.Assert(serverSideAPIRoom != null);
+ Debug.Assert(ServerRoom != null);
+ Debug.Assert(ServerAPIRoom != null);
- var item = serverSidePlaylist.Find(i => i.ID == playlistItemId);
+ var item = ServerRoom.Playlist.FirstOrDefault(i => i.ID == playlistItemId);
if (item == null)
throw new InvalidOperationException("Item does not exist in the room.");
@@ -407,70 +457,78 @@ namespace osu.Game.Tests.Visual.Multiplayer
if (item.Expired)
throw new InvalidOperationException("Attempted to remove an item which has already been played.");
- serverSidePlaylist.Remove(item);
- serverSideAPIRoom.Playlist.RemoveAll(i => i.ID == item.ID);
- await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false);
+ ServerRoom.Playlist.Remove(item);
+ ServerAPIRoom.Playlist.RemoveAll(i => i.ID == item.ID);
+ await ((IMultiplayerClient)this).PlaylistItemRemoved(clone(playlistItemId)).ConfigureAwait(false);
- await updateCurrentItem(Room).ConfigureAwait(false);
+ await updateCurrentItem(ServerRoom).ConfigureAwait(false);
updateRoomStateIfRequired();
}
- public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId);
+ public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId));
private async Task changeMatchType(MatchType type)
{
- Debug.Assert(Room != null);
+ Debug.Assert(ServerRoom != null);
switch (type)
{
case MatchType.HeadToHead:
- await ((IMultiplayerClient)this).MatchRoomStateChanged(null).ConfigureAwait(false);
+ ServerRoom.MatchState = null;
+ await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).ConfigureAwait(false);
+
+ foreach (var user in ServerRoom.Users)
+ {
+ user.MatchState = null;
+ await ((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).ConfigureAwait(false);
+ }
- foreach (var user in Room.Users)
- await ((IMultiplayerClient)this).MatchUserStateChanged(user.UserID, null).ConfigureAwait(false);
break;
case MatchType.TeamVersus:
- await ((IMultiplayerClient)this).MatchRoomStateChanged(TeamVersusRoomState.CreateDefault()).ConfigureAwait(false);
+ ServerRoom.MatchState = TeamVersusRoomState.CreateDefault();
+ await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).ConfigureAwait(false);
+
+ foreach (var user in ServerRoom.Users)
+ {
+ user.MatchState = new TeamVersusUserState();
+ await ((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).ConfigureAwait(false);
+ }
- foreach (var user in Room.Users)
- await ((IMultiplayerClient)this).MatchUserStateChanged(user.UserID, new TeamVersusUserState()).ConfigureAwait(false);
break;
}
}
private async Task changeQueueMode(QueueMode newMode)
{
- Debug.Assert(Room != null);
- Debug.Assert(APIRoom != null);
+ Debug.Assert(ServerRoom != null);
Debug.Assert(currentItem != null);
// When changing to host-only mode, ensure that at least one non-expired playlist item exists by duplicating the current item.
- if (newMode == QueueMode.HostOnly && serverSidePlaylist.All(item => item.Expired))
+ if (newMode == QueueMode.HostOnly && ServerRoom.Playlist.All(item => item.Expired))
await duplicateCurrentItem().ConfigureAwait(false);
- await updatePlaylistOrder(Room).ConfigureAwait(false);
- await updateCurrentItem(Room).ConfigureAwait(false);
+ await updatePlaylistOrder(ServerRoom).ConfigureAwait(false);
+ await updateCurrentItem(ServerRoom).ConfigureAwait(false);
}
public async Task FinishCurrentItem()
{
- Debug.Assert(Room != null);
- Debug.Assert(APIRoom != null);
+ Debug.Assert(ServerRoom != null);
Debug.Assert(currentItem != null);
// Expire the current playlist item.
currentItem.Expired = true;
currentItem.PlayedAt = DateTimeOffset.Now;
- await ((IMultiplayerClient)this).PlaylistItemChanged(currentItem).ConfigureAwait(false);
- await updatePlaylistOrder(Room).ConfigureAwait(false);
+ await ((IMultiplayerClient)this).PlaylistItemChanged(clone(currentItem)).ConfigureAwait(false);
+ await updatePlaylistOrder(ServerRoom).ConfigureAwait(false);
// In host-only mode, a duplicate playlist item will be used for the next round.
- if (Room.Settings.QueueMode == QueueMode.HostOnly && serverSidePlaylist.All(item => item.Expired))
+ if (ServerRoom.Settings.QueueMode == QueueMode.HostOnly && ServerRoom.Playlist.All(item => item.Expired))
await duplicateCurrentItem().ConfigureAwait(false);
- await updateCurrentItem(Room).ConfigureAwait(false);
+ await updateCurrentItem(ServerRoom).ConfigureAwait(false);
}
private async Task duplicateCurrentItem()
@@ -489,51 +547,54 @@ namespace osu.Game.Tests.Visual.Multiplayer
private async Task addItem(MultiplayerPlaylistItem item)
{
- Debug.Assert(Room != null);
- Debug.Assert(serverSideAPIRoom != null);
+ Debug.Assert(ServerRoom != null);
+ Debug.Assert(ServerAPIRoom != null);
item.ID = ++lastPlaylistItemId;
- serverSidePlaylist.Add(item);
- serverSideAPIRoom.Playlist.Add(new PlaylistItem(item));
- await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false);
+ ServerRoom.Playlist.Add(item);
+ ServerAPIRoom.Playlist.Add(new PlaylistItem(item));
+ await ((IMultiplayerClient)this).PlaylistItemAdded(clone(item)).ConfigureAwait(false);
- await updatePlaylistOrder(Room).ConfigureAwait(false);
+ await updatePlaylistOrder(ServerRoom).ConfigureAwait(false);
}
- private IEnumerable upcomingItems => serverSidePlaylist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder);
+ private IEnumerable upcomingItems => ServerRoom?.Playlist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder) ?? Enumerable.Empty();
private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true)
{
- // Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item.
- MultiplayerPlaylistItem nextItem = upcomingItems.FirstOrDefault() ?? serverSidePlaylist.OrderByDescending(i => i.PlayedAt).First();
+ Debug.Assert(ServerRoom != null);
- currentIndex = serverSidePlaylist.IndexOf(nextItem);
+ // Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item.
+ MultiplayerPlaylistItem nextItem = upcomingItems.FirstOrDefault() ?? ServerRoom.Playlist.OrderByDescending(i => i.PlayedAt).First();
+
+ currentIndex = ServerRoom.Playlist.IndexOf(nextItem);
long lastItem = room.Settings.PlaylistItemId;
room.Settings.PlaylistItemId = nextItem.ID;
if (notify && nextItem.ID != lastItem)
- await ((IMultiplayerClient)this).SettingsChanged(room.Settings).ConfigureAwait(false);
+ await ((IMultiplayerClient)this).SettingsChanged(clone(room.Settings)).ConfigureAwait(false);
}
private async Task updatePlaylistOrder(MultiplayerRoom room)
{
- Debug.Assert(serverSideAPIRoom != null);
+ Debug.Assert(ServerRoom != null);
+ Debug.Assert(ServerAPIRoom != null);
List orderedActiveItems;
switch (room.Settings.QueueMode)
{
default:
- orderedActiveItems = serverSidePlaylist.Where(item => !item.Expired).OrderBy(item => item.ID).ToList();
+ orderedActiveItems = ServerRoom.Playlist.Where(item => !item.Expired).OrderBy(item => item.ID).ToList();
break;
case QueueMode.AllPlayersRoundRobin:
var itemsByPriority = new List<(MultiplayerPlaylistItem item, int priority)>();
// Assign a priority for items from each user, starting from 0 and increasing in order which the user added the items.
- foreach (var group in serverSidePlaylist.Where(item => !item.Expired).OrderBy(item => item.ID).GroupBy(item => item.OwnerID))
+ foreach (var group in ServerRoom.Playlist.Where(item => !item.Expired).OrderBy(item => item.ID).GroupBy(item => item.OwnerID))
{
int priority = 0;
itemsByPriority.AddRange(group.Select(item => (item, priority++)));
@@ -564,12 +625,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
item.PlaylistOrder = (ushort)i;
- await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
+ await ((IMultiplayerClient)this).PlaylistItemChanged(clone(item)).ConfigureAwait(false);
}
// Also ensure that the API room's playlist is correct.
- foreach (var item in serverSideAPIRoom.Playlist)
- item.PlaylistOrder = serverSidePlaylist.Single(i => i.ID == item.ID).PlaylistOrder;
+ foreach (var item in ServerAPIRoom.Playlist)
+ item.PlaylistOrder = ServerRoom.Playlist.Single(i => i.ID == item.ID).PlaylistOrder;
+ }
+
+ private T clone(T incoming)
+ {
+ byte[]? serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS);
+ return MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
}
}
}
diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
index 282312c9c1..6577057c17 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
@@ -4,14 +4,14 @@
#nullable disable
using System;
-using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Database;
+using osu.Framework.Logging;
using osu.Game.Beatmaps;
+using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
@@ -72,22 +72,18 @@ namespace osu.Game.Tests.Visual.OnlinePlay
((DummyAPIAccess)API).HandleRequest = request =>
{
- TaskCompletionSource tcs = new TaskCompletionSource();
-
- // Because some of the handlers use realm, we need to ensure the game is still alive when firing.
- // If we don't, a stray `PerformAsync` could hit an `ObjectDisposedException` if running too late.
- Scheduler.Add(() =>
+ try
{
- bool result = handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
- tcs.SetResult(result);
- }, false);
-
-#pragma warning disable RS0030
- // We can't GetResultSafely() here (will fail with "Can't use GetResultSafely from inside an async operation."), but Wait is safe enough due to
- // the task being a TaskCompletionSource.
- // Importantly, this doesn't deadlock because of the scheduler call above running inline where feasible (see the `false` argument).
- return tcs.Task.Result;
-#pragma warning restore RS0030
+ return handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
+ }
+ catch (ObjectDisposedException)
+ {
+ // These requests can be fired asynchronously, but potentially arrive after game components
+ // have been disposed (ie. realm in BeatmapManager).
+ // This only happens in tests and it's easiest to ignore them for now.
+ Logger.Log($"Handled {nameof(ObjectDisposedException)} in test request handling");
+ return true;
+ }
};
});
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
index e7c83ca1f9..fa7ade2c07 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
@@ -136,6 +136,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
return true;
case GetBeatmapsRequest getBeatmapsRequest:
+ {
var result = new List();
foreach (int id in getBeatmapsRequest.BeatmapIds)
@@ -154,6 +155,24 @@ namespace osu.Game.Tests.Visual.OnlinePlay
getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = result });
return true;
+ }
+
+ case GetBeatmapSetRequest getBeatmapSetRequest:
+ {
+ var baseBeatmap = getBeatmapSetRequest.Type == BeatmapSetLookupType.BeatmapId
+ ? beatmapManager.QueryBeatmap(b => b.OnlineID == getBeatmapSetRequest.ID)
+ : beatmapManager.QueryBeatmap(b => b.BeatmapSet.OnlineID == getBeatmapSetRequest.ID);
+
+ if (baseBeatmap == null)
+ {
+ baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo;
+ baseBeatmap.OnlineID = getBeatmapSetRequest.ID;
+ baseBeatmap.BeatmapSet!.OnlineID = getBeatmapSetRequest.ID;
+ }
+
+ getBeatmapSetRequest.TriggerSuccess(OsuTestScene.CreateAPIBeatmapSet(baseBeatmap));
+ return true;
+ }
}
return false;
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/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs
index 521fd8f21a..a9decbae57 100644
--- a/osu.Game/Tests/Visual/PlayerTestScene.cs
+++ b/osu.Game/Tests/Visual/PlayerTestScene.cs
@@ -39,17 +39,17 @@ namespace osu.Game.Tests.Visual
base.SetUpSteps();
if (!HasCustomSteps)
- CreateTest(null);
+ CreateTest();
}
- protected void CreateTest(Action action)
+ protected void CreateTest([CanBeNull] Action action = null)
{
if (action != null && !HasCustomSteps)
throw new InvalidOperationException($"Cannot add custom test steps without {nameof(HasCustomSteps)} being set.");
action?.Invoke();
- AddStep(CreatePlayerRuleset().Description, LoadPlayer);
+ AddStep($"Load player for {CreatePlayerRuleset().Description}", LoadPlayer);
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
}
diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
index fc29c5aac5..2531f3c485 100644
--- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
+++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Spectator
FrameSendAttempts++;
if (ShouldFailSendingFrames)
- return Task.FromException(new InvalidOperationException());
+ return Task.FromException(new InvalidOperationException($"Intentional fail via {nameof(ShouldFailSendingFrames)}"));
return ((ISpectatorClient)this).UserSentFrames(api.LocalUser.Value.Id, bundle);
}
diff --git a/osu.Game/Utils/BatteryInfo.cs b/osu.Game/Utils/BatteryInfo.cs
index be12671b84..dd9b695e1f 100644
--- a/osu.Game/Utils/BatteryInfo.cs
+++ b/osu.Game/Utils/BatteryInfo.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
-
namespace osu.Game.Utils
{
///
diff --git a/osu.Game/Utils/ColourUtils.cs b/osu.Game/Utils/ColourUtils.cs
index 7e665fd9a7..515963971d 100644
--- a/osu.Game/Utils/ColourUtils.cs
+++ b/osu.Game/Utils/ColourUtils.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 osu.Framework.Utils;
using osuTK.Graphics;
diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs
index 07a1879267..799dc75ca9 100644
--- a/osu.Game/Utils/FormatUtils.cs
+++ b/osu.Game/Utils/FormatUtils.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 Humanizer;
using osu.Framework.Extensions.LocalisationExtensions;
diff --git a/osu.Game/Utils/HumanizerUtils.cs b/osu.Game/Utils/HumanizerUtils.cs
index 27d3317b80..5b7c3630d9 100644
--- a/osu.Game/Utils/HumanizerUtils.cs
+++ b/osu.Game/Utils/HumanizerUtils.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.Globalization;
using Humanizer;
diff --git a/osu.Game/Utils/IDeepCloneable.cs b/osu.Game/Utils/IDeepCloneable.cs
index a0a2548f6c..6877f346c4 100644
--- a/osu.Game/Utils/IDeepCloneable.cs
+++ b/osu.Game/Utils/IDeepCloneable.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
-
namespace osu.Game.Utils
{
/// A generic interface for a deeply cloneable type.
diff --git a/osu.Game/Utils/LegacyRandom.cs b/osu.Game/Utils/LegacyRandom.cs
index ace8f8f65c..cf731aa91f 100644
--- a/osu.Game/Utils/LegacyRandom.cs
+++ b/osu.Game/Utils/LegacyRandom.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 osu.Framework.Utils;
diff --git a/osu.Game/Utils/LegacyUtils.cs b/osu.Game/Utils/LegacyUtils.cs
index 400d3b3865..64306adf50 100644
--- a/osu.Game/Utils/LegacyUtils.cs
+++ b/osu.Game/Utils/LegacyUtils.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 osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
diff --git a/osu.Game/Utils/NamingUtils.cs b/osu.Game/Utils/NamingUtils.cs
index 6b1be8885d..482e3d0954 100644
--- a/osu.Game/Utils/NamingUtils.cs
+++ b/osu.Game/Utils/NamingUtils.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.Text.RegularExpressions;
diff --git a/osu.Game/Utils/PeriodTracker.cs b/osu.Game/Utils/PeriodTracker.cs
index d8251f49ca..ba77702247 100644
--- a/osu.Game/Utils/PeriodTracker.cs
+++ b/osu.Game/Utils/PeriodTracker.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;
diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs
index 548aaa887f..3db632fc42 100644
--- a/osu.Game/Utils/StatelessRNG.cs
+++ b/osu.Game/Utils/StatelessRNG.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;
namespace osu.Game.Utils
diff --git a/osu.Game/Utils/ZipUtils.cs b/osu.Game/Utils/ZipUtils.cs
index d6ad3e132e..eb2d2d3b80 100644
--- a/osu.Game/Utils/ZipUtils.cs
+++ b/osu.Game/Utils/ZipUtils.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 SharpCompress.Archives.Zip;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index b9033f1bb9..61fcf2e375 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -20,12 +20,12 @@
-
+
-
-
-
-
+
+
+
+
@@ -36,10 +36,10 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index a1e4cf3ba0..8843b5c831 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,8 +61,8 @@
-
-
+
+
@@ -84,8 +84,8 @@
-
-
+
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index a55b3b5074..0794095854 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -98,6 +98,7 @@
WARNING
HINT
DO_NOT_SHOW
+ HINT
HINT
HINT
ERROR