diff --git a/osu.Android.props b/osu.Android.props
index aad8cf10d0..155a21bacb 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
index 5cbede54f5..41bc075803 100644
--- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
+++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
@@ -49,11 +49,16 @@ namespace osu.Game.Tests.Collections.IO
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
+ // Even with no beatmaps imported, collections are tracking the hashes and will continue to.
+ // In the future this whole mechanism will be replaced with having the collections in realm,
+ // but until that happens it makes rough sense that we want to track not-yet-imported beatmaps
+ // and have them associate with collections if/when they become available.
+
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
- Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.Zero);
+ Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(1));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
- Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.Zero);
+ Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(12));
}
finally
{
@@ -76,10 +81,10 @@ namespace osu.Game.Tests.Collections.IO
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
- Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(1));
+ Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(1));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
- Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(12));
+ Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(12));
}
finally
{
@@ -142,8 +147,8 @@ namespace osu.Game.Tests.Collections.IO
await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
// Move first beatmap from second collection into the first.
- osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]);
- osu.CollectionManager.Collections[1].Beatmaps.RemoveAt(0);
+ osu.CollectionManager.Collections[0].BeatmapHashes.Add(osu.CollectionManager.Collections[1].BeatmapHashes[0]);
+ osu.CollectionManager.Collections[1].BeatmapHashes.RemoveAt(0);
// Rename the second collecction.
osu.CollectionManager.Collections[1].Name.Value = "Another";
@@ -164,10 +169,10 @@ namespace osu.Game.Tests.Collections.IO
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
- Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(2));
+ Assert.That(osu.CollectionManager.Collections[0].BeatmapHashes.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Another"));
- Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(11));
+ Assert.That(osu.CollectionManager.Collections[1].BeatmapHashes.Count, Is.EqualTo(11));
}
finally
{
diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
index 888002eb36..602c7c84b8 100644
--- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
+++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
@@ -152,7 +152,7 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("add two collections with same name", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
- new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
+ new BeatmapCollection { Name = { Value = "1" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
}));
}
@@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
- new BeatmapCollection { Name = { Value = "2" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
+ new BeatmapCollection { Name = { Value = "2" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
}));
assertCollectionCount(2);
@@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Collections
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
- new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
+ new BeatmapCollection { Name = { Value = "1" }, BeatmapHashes = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0].MD5Hash } },
}));
assertCollectionCount(1);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 9d206af40e..c933e1a54e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -18,13 +18,10 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Online
{
- [TestFixture]
public class TestSceneBeatmapSetOverlay : OsuTestScene
{
private readonly TestBeatmapSetOverlay overlay;
- protected override bool UseOnlineAPI => true;
-
private int nextBeatmapSetId = 1;
public TestSceneBeatmapSetOverlay()
@@ -41,12 +38,6 @@ namespace osu.Game.Tests.Visual.Online
AddStep(@"show loading", () => overlay.ShowBeatmapSet(null));
}
- [Test]
- public void TestOnline()
- {
- AddStep(@"show online", () => overlay.FetchAndShowBeatmapSet(55));
- }
-
[Test]
public void TestLocalBeatmaps()
{
@@ -107,6 +98,7 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("status is loved", () => overlay.ChildrenOfType().Single().Status == BeatmapOnlineStatus.Loved);
AddAssert("scores container is visible", () => overlay.ChildrenOfType().Single().Alpha == 1);
+ AddAssert("mod selector is visible", () => overlay.ChildrenOfType().Single().Alpha == 1);
AddStep("go to second beatmap", () => overlay.ChildrenOfType().ElementAt(1).TriggerClick());
diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs
new file mode 100644
index 0000000000..4e88570ca0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapSetOverlay.cs
@@ -0,0 +1,26 @@
+// 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.Overlays;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneOnlineBeatmapSetOverlay : OsuTestScene
+ {
+ private readonly BeatmapSetOverlay overlay;
+
+ protected override bool UseOnlineAPI => true;
+
+ public TestSceneOnlineBeatmapSetOverlay()
+ {
+ Add(overlay = new BeatmapSetOverlay());
+ }
+
+ [Test]
+ public void TestOnline()
+ {
+ AddStep(@"show online", () => overlay.FetchAndShowBeatmapSet(55));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs b/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs
new file mode 100644
index 0000000000..af6681e9cf
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs
@@ -0,0 +1,73 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Screens.Utility;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+ public class TestSceneLatencyCertifierScreen : ScreenTestScene
+ {
+ private LatencyCertifierScreen latencyCertifier = null!;
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("Load screen", () => LoadScreen(latencyCertifier = new LatencyCertifierScreen()));
+ AddUntilStep("wait for load", () => latencyCertifier.IsLoaded);
+ }
+
+ [Test]
+ public void TestCertification()
+ {
+ checkDifficulty(1);
+ clickUntilResults(true);
+ continueFromResults();
+ checkDifficulty(2);
+
+ clickUntilResults(false);
+ continueFromResults();
+ checkDifficulty(1);
+
+ clickUntilResults(true);
+ AddAssert("check at results", () => !latencyCertifier.ChildrenOfType().Any());
+ AddAssert("check no buttons", () => !latencyCertifier.ChildrenOfType().Any());
+ checkDifficulty(1);
+ }
+
+ private void continueFromResults()
+ {
+ AddAssert("check at results", () => !latencyCertifier.ChildrenOfType().Any());
+ AddStep("hit enter to continue", () => InputManager.Key(Key.Enter));
+ }
+
+ private void checkDifficulty(int difficulty)
+ {
+ AddAssert($"difficulty is {difficulty}", () => latencyCertifier.DifficultyLevel == difficulty);
+ }
+
+ private void clickUntilResults(bool clickCorrect)
+ {
+ AddUntilStep("click correct button until results", () =>
+ {
+ var latencyArea = latencyCertifier
+ .ChildrenOfType()
+ .SingleOrDefault(a => clickCorrect ? a.TargetFrameRate == null : a.TargetFrameRate != null);
+
+ // reached results
+ if (latencyArea == null)
+ return true;
+
+ latencyArea.ChildrenOfType().Single().TriggerClick();
+ return false;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
index b7ec128596..b42ce3ff87 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
@@ -151,10 +151,10 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
- AddStep("add beatmap to collection", () => collectionManager.Collections[0].Beatmaps.Add(Beatmap.Value.BeatmapInfo));
+ AddStep("add beatmap to collection", () => collectionManager.Collections[0].BeatmapHashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
- AddStep("remove beatmap from collection", () => collectionManager.Collections[0].Beatmaps.Clear());
+ AddStep("remove beatmap from collection", () => collectionManager.Collections[0].BeatmapHashes.Clear());
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
}
@@ -169,11 +169,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
addClickAddOrRemoveButtonStep(1);
- AddAssert("collection contains beatmap", () => collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo));
+ AddAssert("collection contains beatmap", () => collectionManager.Collections[0].BeatmapHashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
addClickAddOrRemoveButtonStep(1);
- AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo));
+ AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].BeatmapHashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index abc9020dc6..5925dd7064 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -90,6 +90,7 @@ namespace osu.Game.Beatmaps
public double StarRating { get; set; }
+ [Indexed]
public string MD5Hash { get; set; } = string.Empty;
[JsonIgnore]
diff --git a/osu.Game/Collections/BeatmapCollection.cs b/osu.Game/Collections/BeatmapCollection.cs
index 7e4b15ecf9..cf95c74b46 100644
--- a/osu.Game/Collections/BeatmapCollection.cs
+++ b/osu.Game/Collections/BeatmapCollection.cs
@@ -23,9 +23,9 @@ namespace osu.Game.Collections
public readonly Bindable Name = new Bindable();
///
- /// The beatmaps contained by the collection.
+ /// The es of beatmaps contained by the collection.
///
- public readonly BindableList Beatmaps = new BindableList();
+ public readonly BindableList BeatmapHashes = new BindableList();
///
/// The date when this collection was last modified.
@@ -34,7 +34,7 @@ namespace osu.Game.Collections
public BeatmapCollection()
{
- Beatmaps.CollectionChanged += (_, __) => onChange();
+ BeatmapHashes.CollectionChanged += (_, __) => onChange();
Name.ValueChanged += _ => onChange();
}
diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs
index c46ba8e06e..100074d186 100644
--- a/osu.Game/Collections/CollectionFilterDropdown.cs
+++ b/osu.Game/Collections/CollectionFilterDropdown.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Collections
}
private readonly IBindableList collections = new BindableList();
- private readonly IBindableList beatmaps = new BindableList();
+ private readonly IBindableList beatmaps = new BindableList();
private readonly BindableList filters = new BindableList();
[Resolved(CanBeNull = true)]
@@ -95,10 +95,10 @@ namespace osu.Game.Collections
beatmaps.CollectionChanged -= filterBeatmapsChanged;
if (filter.OldValue?.Collection != null)
- beatmaps.UnbindFrom(filter.OldValue.Collection.Beatmaps);
+ beatmaps.UnbindFrom(filter.OldValue.Collection.BeatmapHashes);
if (filter.NewValue?.Collection != null)
- beatmaps.BindTo(filter.NewValue.Collection.Beatmaps);
+ beatmaps.BindTo(filter.NewValue.Collection.BeatmapHashes);
beatmaps.CollectionChanged += filterBeatmapsChanged;
@@ -196,7 +196,7 @@ namespace osu.Game.Collections
private IBindable beatmap { get; set; }
[CanBeNull]
- private readonly BindableList collectionBeatmaps;
+ private readonly BindableList collectionBeatmaps;
[NotNull]
private readonly Bindable collectionName;
@@ -208,7 +208,7 @@ namespace osu.Game.Collections
public CollectionDropdownMenuItem(MenuItem item)
: base(item)
{
- collectionBeatmaps = Item.Collection?.Beatmaps.GetBoundCopy();
+ collectionBeatmaps = Item.Collection?.BeatmapHashes.GetBoundCopy();
collectionName = Item.CollectionName.GetBoundCopy();
}
@@ -258,7 +258,7 @@ namespace osu.Game.Collections
{
Debug.Assert(collectionBeatmaps != null);
- beatmapInCollection = collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo);
+ beatmapInCollection = collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo.MD5Hash);
addOrRemoveButton.Enabled.Value = !beatmap.IsDefault;
addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare;
@@ -285,8 +285,8 @@ namespace osu.Game.Collections
{
Debug.Assert(collectionBeatmaps != null);
- if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo))
- collectionBeatmaps.Add(beatmap.Value.BeatmapInfo);
+ if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
+ collectionBeatmaps.Add(beatmap.Value.BeatmapInfo.MD5Hash);
}
protected override Drawable CreateContent() => content = (Content)base.CreateContent();
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index 700b0f5dcb..104ec4beb2 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -13,7 +13,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
-using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Legacy;
@@ -40,9 +39,6 @@ namespace osu.Game.Collections
public readonly BindableList Collections = new BindableList();
- [Resolved]
- private BeatmapManager beatmaps { get; set; }
-
private readonly Storage storage;
public CollectionManager(Storage storage)
@@ -173,10 +169,10 @@ namespace osu.Game.Collections
if (existing == null)
Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
- foreach (var newBeatmap in newCol.Beatmaps)
+ foreach (string newBeatmap in newCol.BeatmapHashes)
{
- if (!existing.Beatmaps.Contains(newBeatmap))
- existing.Beatmaps.Add(newBeatmap);
+ if (!existing.BeatmapHashes.Contains(newBeatmap))
+ existing.BeatmapHashes.Add(newBeatmap);
}
}
@@ -226,9 +222,7 @@ namespace osu.Game.Collections
string checksum = sr.ReadString();
- var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum);
- if (beatmap != null)
- collection.Beatmaps.Add(beatmap);
+ collection.BeatmapHashes.Add(checksum);
}
if (notification != null)
@@ -299,11 +293,12 @@ namespace osu.Game.Collections
{
sw.Write(c.Name.Value);
- var beatmapsCopy = c.Beatmaps.ToArray();
+ string[] beatmapsCopy = c.BeatmapHashes.ToArray();
+
sw.Write(beatmapsCopy.Length);
- foreach (var b in beatmapsCopy)
- sw.Write(b.MD5Hash);
+ foreach (string b in beatmapsCopy)
+ sw.Write(b);
}
}
diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs
index e5a2f6fb81..e59adb14a6 100644
--- a/osu.Game/Collections/DeleteCollectionDialog.cs
+++ b/osu.Game/Collections/DeleteCollectionDialog.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Collections
public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction)
{
HeaderText = "Confirm deletion of";
- BodyText = $"{collection.Name.Value} ({"beatmap".ToQuantity(collection.Beatmaps.Count)})";
+ BodyText = $"{collection.Name.Value} ({"beatmap".ToQuantity(collection.BeatmapHashes.Count)})";
Icon = FontAwesome.Regular.TrashAlt;
diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs
index 5a20b7e7bd..5064041737 100644
--- a/osu.Game/Collections/DrawableCollectionListItem.cs
+++ b/osu.Game/Collections/DrawableCollectionListItem.cs
@@ -225,7 +225,7 @@ namespace osu.Game.Collections
{
background.FlashColour(Color4.White, 150);
- if (collection.Beatmaps.Count == 0)
+ if (collection.BeatmapHashes.Count == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index 5f24a6549d..ebfb8f124b 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -242,8 +242,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
modSelector.DeselectAll();
else
getScores();
-
- modSelector.FadeTo(userIsSupporter ? 1 : 0);
}
private void getScores()
@@ -260,7 +258,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return;
}
- if (scope.Value != BeatmapLeaderboardScope.Global && !userIsSupporter)
+ if ((scope.Value != BeatmapLeaderboardScope.Global || modSelector.SelectedMods.Count > 0) && !userIsSupporter)
{
Scores = null;
notSupporterPlaceholder.Show();
diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs
index 24d867141c..ff8f3197f9 100644
--- a/osu.Game/Overlays/Music/Playlist.cs
+++ b/osu.Game/Overlays/Music/Playlist.cs
@@ -30,7 +30,15 @@ namespace osu.Game.Overlays.Music
var items = (SearchContainer>>)ListContainer;
foreach (var item in items.OfType())
- item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => item.Model.ID == b.BeatmapSet?.ID) ?? true;
+ {
+ if (criteria.Collection == null)
+ item.InSelectedCollection = true;
+ else
+ {
+ item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash)
+ .Any(criteria.Collection.BeatmapHashes.Contains);
+ }
+ }
items.SearchTerm = criteria.SearchText;
}
diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs
index 8833420523..2b845e9d6b 100644
--- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs
@@ -9,6 +9,7 @@ using osu.Framework.Screens;
using osu.Game.Localisation;
using osu.Game.Screens;
using osu.Game.Screens.Import;
+using osu.Game.Screens.Utility;
namespace osu.Game.Overlays.Settings.Sections.DebugSettings
{
@@ -30,13 +31,18 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
{
LabelText = DebugSettingsStrings.BypassFrontToBackPass,
Current = config.GetBindable(DebugSetting.BypassFrontToBackPass)
+ },
+ new SettingsButton
+ {
+ Text = DebugSettingsStrings.ImportFiles,
+ Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen()))
+ },
+ new SettingsButton
+ {
+ Text = @"Run latency certifier",
+ Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen()))
}
};
- Add(new SettingsButton
- {
- Text = DebugSettingsStrings.ImportFiles,
- Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen()))
- });
}
}
}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index dfc0fa1d1d..d6f6a06eb7 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -181,7 +182,7 @@ namespace osu.Game.Screens.Play
}
[BackgroundDependencyLoader(true)]
- private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
+ private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game, CancellationToken cancellationToken)
{
var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray();
@@ -194,7 +195,7 @@ namespace osu.Game.Screens.Play
if (Beatmap.Value is DummyWorkingBeatmap)
return;
- IBeatmap playableBeatmap = loadPlayableBeatmap(gameplayMods);
+ IBeatmap playableBeatmap = loadPlayableBeatmap(gameplayMods, cancellationToken);
if (playableBeatmap == null)
return;
@@ -483,7 +484,7 @@ namespace osu.Game.Screens.Play
}
}
- private IBeatmap loadPlayableBeatmap(Mod[] gameplayMods)
+ private IBeatmap loadPlayableBeatmap(Mod[] gameplayMods, CancellationToken cancellationToken)
{
IBeatmap playable;
@@ -500,7 +501,7 @@ namespace osu.Game.Screens.Play
try
{
- playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods);
+ playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods, cancellationToken);
}
catch (BeatmapInvalidForRulesetException)
{
@@ -508,7 +509,7 @@ namespace osu.Game.Screens.Play
rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance();
- playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods);
+ playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods, cancellationToken);
}
if (playable.HitObjects.Count == 0)
@@ -517,6 +518,11 @@ namespace osu.Game.Screens.Play
return null;
}
}
+ catch (OperationCanceledException)
+ {
+ // Load has been cancelled. No logging is required.
+ return null;
+ }
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap successfully!");
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index fd6a869938..c3f6b3ad83 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Screens.Select.Carousel
}
if (match)
- match &= criteria.Collection?.Beatmaps.Contains(BeatmapInfo) ?? true;
+ match &= criteria.Collection?.BeatmapHashes.Contains(BeatmapInfo.MD5Hash) ?? true;
if (match && criteria.RulesetCriteria != null)
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
index 98b885eb43..065a29b53c 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
@@ -256,12 +256,12 @@ namespace osu.Game.Screens.Select.Carousel
return new ToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s =>
{
if (s)
- collection.Beatmaps.Add(beatmapInfo);
+ collection.BeatmapHashes.Add(beatmapInfo.MD5Hash);
else
- collection.Beatmaps.Remove(beatmapInfo);
+ collection.BeatmapHashes.Remove(beatmapInfo.MD5Hash);
})
{
- State = { Value = collection.Beatmaps.Contains(beatmapInfo) }
+ State = { Value = collection.BeatmapHashes.Contains(beatmapInfo.MD5Hash) }
};
}
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs
index 2d70b1aecb..80f1231454 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs
@@ -245,7 +245,7 @@ namespace osu.Game.Screens.Select.Carousel
TernaryState state;
- int countExisting = beatmapSet.Beatmaps.Count(b => collection.Beatmaps.Contains(b));
+ int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapHashes.Contains(b.MD5Hash));
if (countExisting == beatmapSet.Beatmaps.Count)
state = TernaryState.True;
@@ -261,14 +261,14 @@ namespace osu.Game.Screens.Select.Carousel
switch (s)
{
case TernaryState.True:
- if (collection.Beatmaps.Contains(b))
+ if (collection.BeatmapHashes.Contains(b.MD5Hash))
continue;
- collection.Beatmaps.Add(b);
+ collection.BeatmapHashes.Add(b.MD5Hash);
break;
case TernaryState.False:
- collection.Beatmaps.Remove(b);
+ collection.BeatmapHashes.Remove(b.MD5Hash);
break;
}
}
diff --git a/osu.Game/Screens/Utility/ButtonWithKeyBind.cs b/osu.Game/Screens/Utility/ButtonWithKeyBind.cs
new file mode 100644
index 0000000000..ef87e0bca7
--- /dev/null
+++ b/osu.Game/Screens/Utility/ButtonWithKeyBind.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+using osu.Framework.Allocation;
+using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Settings;
+using osuTK.Input;
+
+namespace osu.Game.Screens.Utility
+{
+ public class ButtonWithKeyBind : SettingsButton
+ {
+ private readonly Key key;
+
+ public ButtonWithKeyBind(Key key)
+ {
+ this.key = key;
+ }
+
+ public new LocalisableString Text
+ {
+ get => base.Text;
+ set => base.Text = $"{value} (Press {key.ToString().Replace("Number", string.Empty)})";
+ }
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (!e.Repeat && e.Key == key)
+ {
+ TriggerClick();
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
+ [Resolved]
+ private OverlayColourProvider overlayColourProvider { get; set; } = null!;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Height = 100;
+ SpriteText.Colour = overlayColourProvider.Background6;
+ SpriteText.Font = OsuFont.TorusAlternate.With(size: 34);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Utility/LatencyArea.cs b/osu.Game/Screens/Utility/LatencyArea.cs
new file mode 100644
index 0000000000..2ef48bb571
--- /dev/null
+++ b/osu.Game/Screens/Utility/LatencyArea.cs
@@ -0,0 +1,241 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+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.Overlays;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Screens.Utility
+{
+ public class LatencyArea : CompositeDrawable
+ {
+ [Resolved]
+ private OverlayColourProvider overlayColourProvider { get; set; } = null!;
+
+ public Action? ReportUserBest { get; set; }
+
+ private Drawable? background;
+
+ private readonly Key key;
+
+ public readonly int? TargetFrameRate;
+
+ public readonly BindableBool IsActiveArea = new BindableBool();
+
+ public LatencyArea(Key key, int? targetFrameRate)
+ {
+ this.key = key;
+ TargetFrameRate = targetFrameRate;
+
+ RelativeSizeAxes = Axes.Both;
+ Masking = true;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ InternalChildren = new[]
+ {
+ background = new Box
+ {
+ Colour = overlayColourProvider.Background6,
+ RelativeSizeAxes = Axes.Both,
+ },
+ new ButtonWithKeyBind(key)
+ {
+ Text = "Feels better",
+ Y = 20,
+ Width = 0.8f,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Action = () => ReportUserBest?.Invoke(),
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new LatencyMovableBox(IsActiveArea)
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new LatencyCursorContainer(IsActiveArea)
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ }
+ },
+ };
+
+ IsActiveArea.BindValueChanged(active =>
+ {
+ background.FadeColour(active.NewValue ? overlayColourProvider.Background4 : overlayColourProvider.Background6, 200, Easing.OutQuint);
+ }, true);
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ IsActiveArea.Value = true;
+ return base.OnMouseMove(e);
+ }
+
+ private double lastFrameTime;
+
+ public override bool UpdateSubTree()
+ {
+ double elapsed = Clock.CurrentTime - lastFrameTime;
+ if (TargetFrameRate.HasValue && elapsed < 1000.0 / TargetFrameRate)
+ return false;
+
+ lastFrameTime = Clock.CurrentTime;
+
+ return base.UpdateSubTree();
+ }
+
+ public class LatencyMovableBox : CompositeDrawable
+ {
+ private Box box = null!;
+ private InputManager inputManager = null!;
+
+ private readonly BindableBool isActive;
+
+ [Resolved]
+ private OverlayColourProvider overlayColourProvider { get; set; } = null!;
+
+ public LatencyMovableBox(BindableBool isActive)
+ {
+ this.isActive = isActive;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+
+ InternalChild = box = new Box
+ {
+ Size = new Vector2(40),
+ RelativePositionAxes = Axes.Both,
+ Position = new Vector2(0.5f),
+ Origin = Anchor.Centre,
+ Colour = overlayColourProvider.Colour1,
+ };
+ }
+
+ protected override bool OnHover(HoverEvent e) => false;
+
+ private double? lastFrameTime;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!isActive.Value)
+ {
+ lastFrameTime = null;
+ box.Colour = overlayColourProvider.Colour1;
+ return;
+ }
+
+ if (lastFrameTime != null)
+ {
+ float movementAmount = (float)(Clock.CurrentTime - lastFrameTime) / 400;
+
+ var buttons = inputManager.CurrentState.Keyboard.Keys;
+
+ box.Colour = buttons.HasAnyButtonPressed ? overlayColourProvider.Content1 : overlayColourProvider.Colour1;
+
+ foreach (var key in buttons)
+ {
+ switch (key)
+ {
+ case Key.K:
+ case Key.Up:
+ box.Y = MathHelper.Clamp(box.Y - movementAmount, 0.1f, 0.9f);
+ break;
+
+ case Key.J:
+ case Key.Down:
+ box.Y = MathHelper.Clamp(box.Y + movementAmount, 0.1f, 0.9f);
+ break;
+
+ case Key.Z:
+ case Key.Left:
+ box.X = MathHelper.Clamp(box.X - movementAmount, 0.1f, 0.9f);
+ break;
+
+ case Key.X:
+ case Key.Right:
+ box.X = MathHelper.Clamp(box.X + movementAmount, 0.1f, 0.9f);
+ break;
+ }
+ }
+ }
+
+ lastFrameTime = Clock.CurrentTime;
+ }
+ }
+
+ public class LatencyCursorContainer : CompositeDrawable
+ {
+ private Circle cursor = null!;
+ private InputManager inputManager = null!;
+
+ private readonly BindableBool isActive;
+
+ [Resolved]
+ private OverlayColourProvider overlayColourProvider { get; set; } = null!;
+
+ public LatencyCursorContainer(BindableBool isActive)
+ {
+ this.isActive = isActive;
+ Masking = true;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ InternalChild = cursor = new Circle
+ {
+ Size = new Vector2(40),
+ Origin = Anchor.Centre,
+ Colour = overlayColourProvider.Colour2,
+ };
+
+ inputManager = GetContainingInputManager();
+ }
+
+ protected override bool OnHover(HoverEvent e) => false;
+
+ protected override void Update()
+ {
+ cursor.Colour = inputManager.CurrentState.Mouse.IsPressed(MouseButton.Left) ? overlayColourProvider.Content1 : overlayColourProvider.Colour2;
+
+ if (isActive.Value)
+ {
+ cursor.Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
+ cursor.Alpha = 1;
+ }
+ else
+ {
+ cursor.Alpha = 0;
+ }
+
+ base.Update();
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs
new file mode 100644
index 0000000000..0a9d98450f
--- /dev/null
+++ b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs
@@ -0,0 +1,461 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using osu.Framework.Allocation;
+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.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Framework.Platform;
+using osu.Framework.Platform.Windows;
+using osu.Framework.Screens;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Screens.Utility
+{
+ public class LatencyCertifierScreen : OsuScreen
+ {
+ private FrameSync previousFrameSyncMode;
+ private double previousActiveHz;
+
+ private readonly OsuTextFlowContainer statusText;
+
+ public override bool HideOverlaysOnEnter => true;
+
+ public override bool CursorVisible => mainArea.Count == 0;
+
+ public override float BackgroundParallaxAmount => 0;
+
+ private readonly OsuTextFlowContainer explanatoryText;
+
+ private readonly Container mainArea;
+
+ private readonly Container resultsArea;
+
+ ///
+ /// The rate at which the game host should attempt to run.
+ ///
+ private const int target_host_update_frames = 4000;
+
+ [Cached]
+ private readonly OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Orange);
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ [Resolved]
+ private FrameworkConfigManager config { get; set; } = null!;
+
+ private const int rounds_to_complete = 5;
+
+ private const int rounds_to_complete_certified = 20;
+
+ ///
+ /// Whether we are now in certification mode and decreasing difficulty.
+ ///
+ private bool isCertifying;
+
+ private int totalRoundForNextResultsScreen => isCertifying ? rounds_to_complete_certified : rounds_to_complete;
+
+ private int attemptsAtCurrentDifficulty;
+ private int correctAtCurrentDifficulty;
+
+ public int DifficultyLevel { get; private set; } = 1;
+
+ private double lastPoll;
+ private int pollingMax;
+
+ [Resolved]
+ private GameHost host { get; set; } = null!;
+
+ public LatencyCertifierScreen()
+ {
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = overlayColourProvider.Background6,
+ RelativeSizeAxes = Axes.Both,
+ },
+ mainArea = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ // Make sure the edge between the two comparisons can't be used to ascertain latency.
+ new Box
+ {
+ Name = "separator",
+ Colour = ColourInfo.GradientHorizontal(overlayColourProvider.Background6, overlayColourProvider.Background6.Opacity(0)),
+ Width = 100,
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopLeft,
+ },
+ new Box
+ {
+ Name = "separator",
+ Colour = ColourInfo.GradientHorizontal(overlayColourProvider.Background6.Opacity(0), overlayColourProvider.Background6),
+ Width = 100,
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopRight,
+ },
+ explanatoryText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ TextAnchor = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Text = @"Welcome to the latency certifier!
+Use the arrow keys, Z/X/J/K to move the square.
+Use the Tab key to change focus.
+Do whatever you need to try and perceive the difference in latency, then choose your best side.
+",
+ },
+ resultsArea = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ statusText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 40))
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ TextAnchor = Anchor.TopCentre,
+ Y = 150,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ };
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ if (lastPoll > 0)
+ pollingMax = (int)Math.Max(pollingMax, 1000 / (Clock.CurrentTime - lastPoll));
+ lastPoll = Clock.CurrentTime;
+ return base.OnMouseMove(e);
+ }
+
+ public override void OnEntering(ScreenTransitionEvent e)
+ {
+ base.OnEntering(e);
+
+ previousFrameSyncMode = config.Get(FrameworkSetting.FrameSync);
+ previousActiveHz = host.UpdateThread.ActiveHz;
+ config.SetValue(FrameworkSetting.FrameSync, FrameSync.Unlimited);
+ host.UpdateThread.ActiveHz = target_host_update_frames;
+ host.AllowBenchmarkUnlimitedFrames = true;
+ }
+
+ public override bool OnExiting(ScreenExitEvent e)
+ {
+ host.AllowBenchmarkUnlimitedFrames = false;
+ config.SetValue(FrameworkSetting.FrameSync, previousFrameSyncMode);
+ host.UpdateThread.ActiveHz = previousActiveHz;
+ return base.OnExiting(e);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ loadNextRound();
+ }
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ switch (e.Key)
+ {
+ case Key.Tab:
+ var firstArea = mainArea.FirstOrDefault(a => !a.IsActiveArea.Value);
+ if (firstArea != null)
+ firstArea.IsActiveArea.Value = true;
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
+ private void showResults()
+ {
+ mainArea.Clear();
+
+ var displayMode = host.Window?.CurrentDisplayMode.Value;
+
+ string exclusive = "unknown";
+
+ if (host.Window is WindowsWindow windowsWindow)
+ exclusive = windowsWindow.FullscreenCapability.ToString();
+
+ statusText.Clear();
+
+ float successRate = (float)correctAtCurrentDifficulty / attemptsAtCurrentDifficulty;
+ bool isPass = successRate == 1;
+
+ statusText.AddParagraph($"You scored {correctAtCurrentDifficulty} out of {attemptsAtCurrentDifficulty} ({successRate:0%})!", cp => cp.Colour = isPass ? colours.Green : colours.Red);
+ statusText.AddParagraph($"Level {DifficultyLevel} ({mapDifficultyToTargetFrameRate(DifficultyLevel):N0} Hz)",
+ cp => cp.Font = OsuFont.Default.With(size: 24));
+
+ statusText.AddParagraph(string.Empty);
+ statusText.AddParagraph(string.Empty);
+ statusText.AddIcon(isPass ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.TimesCircle, cp => cp.Colour = isPass ? colours.Green : colours.Red);
+ statusText.AddParagraph(string.Empty);
+
+ if (!isPass && DifficultyLevel > 1)
+ {
+ statusText.AddParagraph("To complete certification, the difficulty level will now decrease until you can get 20 rounds correct in a row!",
+ cp => cp.Font = OsuFont.Default.With(size: 24, weight: FontWeight.SemiBold));
+ statusText.AddParagraph(string.Empty);
+ }
+
+ statusText.AddParagraph($"Polling: {pollingMax} Hz Monitor: {displayMode?.RefreshRate ?? 0:N0} Hz Exclusive: {exclusive}",
+ cp => cp.Font = OsuFont.Default.With(size: 15, weight: FontWeight.SemiBold));
+
+ statusText.AddParagraph($"Input: {host.InputThread.Clock.FramesPerSecond} Hz "
+ + $"Update: {host.UpdateThread.Clock.FramesPerSecond} Hz "
+ + $"Draw: {host.DrawThread.Clock.FramesPerSecond} Hz"
+ , cp => cp.Font = OsuFont.Default.With(size: 15, weight: FontWeight.SemiBold));
+
+ if (isCertifying && isPass)
+ {
+ showCertifiedScreen();
+ return;
+ }
+
+ string cannotIncreaseReason = string.Empty;
+
+ if (mapDifficultyToTargetFrameRate(DifficultyLevel + 1) > target_host_update_frames)
+ cannotIncreaseReason = "You've reached the maximum level.";
+ else if (mapDifficultyToTargetFrameRate(DifficultyLevel + 1) > Clock.FramesPerSecond)
+ cannotIncreaseReason = "Game is not running fast enough to test this level";
+
+ FillFlowContainer buttonFlow;
+
+ resultsArea.Add(buttonFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Spacing = new Vector2(20),
+ Padding = new MarginPadding(20),
+ });
+
+ if (isPass)
+ {
+ buttonFlow.Add(new ButtonWithKeyBind(Key.Enter)
+ {
+ Text = "Continue to next level",
+ BackgroundColour = colours.Green,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Action = () => changeDifficulty(DifficultyLevel + 1),
+ Enabled = { Value = string.IsNullOrEmpty(cannotIncreaseReason) },
+ TooltipText = cannotIncreaseReason
+ });
+ }
+ else
+ {
+ if (DifficultyLevel == 1)
+ {
+ buttonFlow.Add(new ButtonWithKeyBind(Key.Enter)
+ {
+ Text = "Retry",
+ TooltipText = "Are you even trying..?",
+ BackgroundColour = colours.Pink2,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Action = () =>
+ {
+ isCertifying = false;
+ changeDifficulty(1);
+ },
+ });
+ }
+ else
+ {
+ buttonFlow.Add(new ButtonWithKeyBind(Key.Enter)
+ {
+ Text = "Begin certification at last level",
+ BackgroundColour = colours.Yellow,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Action = () =>
+ {
+ isCertifying = true;
+ changeDifficulty(DifficultyLevel - 1);
+ },
+ TooltipText = isPass ? $"Chain {rounds_to_complete_certified} rounds to confirm your perception!" : "You've reached your limits. Go to the previous level to complete certification!",
+ });
+ }
+ }
+ }
+
+ private void showCertifiedScreen()
+ {
+ Drawable background;
+ Drawable certifiedText;
+
+ resultsArea.AddRange(new[]
+ {
+ background = new Box
+ {
+ Colour = overlayColourProvider.Background4,
+ RelativeSizeAxes = Axes.Both,
+ },
+ (certifiedText = new OsuSpriteText
+ {
+ Alpha = 0,
+ Font = OsuFont.TorusAlternate.With(size: 80, weight: FontWeight.Bold),
+ Text = "Certified!",
+ Blending = BlendingParameters.Additive,
+ }).WithEffect(new GlowEffect
+ {
+ Colour = overlayColourProvider.Colour1,
+ PadExtent = true
+ }).With(e =>
+ {
+ e.Anchor = Anchor.Centre;
+ e.Origin = Anchor.Centre;
+ }),
+ new OsuSpriteText
+ {
+ Text = $"You should use a frame limiter with update rate of {mapDifficultyToTargetFrameRate(DifficultyLevel + 1)} Hz (or fps) for best results!",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold),
+ Y = 80,
+ }
+ });
+
+ background.FadeInFromZero(1000, Easing.OutQuint);
+
+ certifiedText.FadeInFromZero(500, Easing.InQuint);
+
+ certifiedText
+ .ScaleTo(10)
+ .ScaleTo(1, 600, Easing.InQuad)
+ .Then()
+ .ScaleTo(1.05f, 10000, Easing.OutQuint);
+ }
+
+ private void changeDifficulty(int difficulty)
+ {
+ Debug.Assert(difficulty > 0);
+
+ resultsArea.Clear();
+
+ correctAtCurrentDifficulty = 0;
+ attemptsAtCurrentDifficulty = 0;
+
+ pollingMax = 0;
+ lastPoll = 0;
+
+ DifficultyLevel = difficulty;
+
+ loadNextRound();
+ }
+
+ private void loadNextRound()
+ {
+ attemptsAtCurrentDifficulty++;
+ statusText.Text = $"Level {DifficultyLevel}\nRound {attemptsAtCurrentDifficulty} of {totalRoundForNextResultsScreen}";
+
+ mainArea.Clear();
+
+ int betterSide = RNG.Next(0, 2);
+
+ mainArea.AddRange(new[]
+ {
+ new LatencyArea(Key.Number1, betterSide == 1 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : (int?)null)
+ {
+ Width = 0.5f,
+ IsActiveArea = { Value = true },
+ ReportUserBest = () => recordResult(betterSide == 0),
+ },
+ new LatencyArea(Key.Number2, betterSide == 0 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : (int?)null)
+ {
+ Width = 0.5f,
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ ReportUserBest = () => recordResult(betterSide == 1)
+ }
+ });
+
+ foreach (var area in mainArea)
+ {
+ area.IsActiveArea.BindValueChanged(active =>
+ {
+ if (active.NewValue)
+ mainArea.Children.First(a => a != area).IsActiveArea.Value = false;
+ });
+ }
+ }
+
+ private void recordResult(bool correct)
+ {
+ // Fading this out will improve the frame rate after the first round due to less text on screen.
+ explanatoryText.FadeOut(500, Easing.OutQuint);
+
+ if (correct)
+ correctAtCurrentDifficulty++;
+
+ if (attemptsAtCurrentDifficulty < totalRoundForNextResultsScreen)
+ loadNextRound();
+ else
+ showResults();
+ }
+
+ private static int mapDifficultyToTargetFrameRate(int difficulty)
+ {
+ switch (difficulty)
+ {
+ case 1:
+ return 15;
+
+ case 2:
+ return 30;
+
+ case 3:
+ return 45;
+
+ case 4:
+ return 60;
+
+ case 5:
+ return 120;
+
+ case 6:
+ return 240;
+
+ case 7:
+ return 480;
+
+ case 8:
+ return 720;
+
+ case 9:
+ return 960;
+
+ default:
+ return 1000 + ((difficulty - 10) * 500);
+ }
+ }
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 63b8cf4cb5..b6218c5950 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index a0fafa635b..ba57aba01b 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -84,7 +84,7 @@
-
+