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