diff --git a/osu.Game.Benchmarks/BenchmarkRuleset.cs b/osu.Game.Benchmarks/BenchmarkRuleset.cs index 63c99dcb2b..2835ec9499 100644 --- a/osu.Game.Benchmarks/BenchmarkRuleset.cs +++ b/osu.Game.Benchmarks/BenchmarkRuleset.cs @@ -4,6 +4,7 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; using osu.Game.Online.API; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; namespace osu.Game.Benchmarks @@ -37,7 +38,25 @@ namespace osu.Game.Benchmarks [Benchmark] public void BenchmarkGetAllMods() { - ruleset.GetAllMods().Consume(new Consumer()); + ruleset.CreateAllMods().Consume(new Consumer()); + } + + [Benchmark] + public void BenchmarkGetAllModsForReference() + { + ruleset.AllMods.Consume(new Consumer()); + } + + [Benchmark] + public void BenchmarkGetForAcronym() + { + ruleset.CreateModFromAcronym("DT"); + } + + [Benchmark] + public void BenchmarkGetForType() + { + ruleset.CreateMod(); } } } diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs index 7a5789f01a..ce6b3a68a5 100644 --- a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs +++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Game.Online.API; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Tests.Mods @@ -11,26 +12,42 @@ namespace osu.Game.Tests.Mods public class ModSettingsEqualityComparison { [Test] - public void Test() + public void TestAPIMod() { + var apiMod1 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.25 } }); + var apiMod2 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }); + var apiMod3 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }); + + Assert.That(apiMod1, Is.Not.EqualTo(apiMod2)); + Assert.That(apiMod2, Is.EqualTo(apiMod2)); + Assert.That(apiMod2, Is.EqualTo(apiMod3)); + Assert.That(apiMod3, Is.EqualTo(apiMod2)); + } + + [Test] + public void TestMod() + { + var ruleset = new OsuRuleset(); + var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } }; var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }; var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }; - var apiMod1 = new APIMod(mod1); - var apiMod2 = new APIMod(mod2); - var apiMod3 = new APIMod(mod3); + + var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset); + var doulbeConvertedMod2 = new APIMod(mod2).ToMod(ruleset); + var doulbeConvertedMod3 = new APIMod(mod3).ToMod(ruleset); Assert.That(mod1, Is.Not.EqualTo(mod2)); - Assert.That(apiMod1, Is.Not.EqualTo(apiMod2)); + Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doulbeConvertedMod2)); Assert.That(mod2, Is.EqualTo(mod2)); - Assert.That(apiMod2, Is.EqualTo(apiMod2)); + Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod2)); Assert.That(mod2, Is.EqualTo(mod3)); - Assert.That(apiMod2, Is.EqualTo(apiMod3)); + Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod3)); Assert.That(mod3, Is.EqualTo(mod2)); - Assert.That(apiMod3, Is.EqualTo(apiMod2)); + Assert.That(doulbeConvertedMod3, Is.EqualTo(doulbeConvertedMod2)); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs index b7dcad3825..00b5c38e20 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Configuration; @@ -71,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay var working = CreateWorkingBeatmap(rulesetInfo); Beatmap.Value = working; - SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; + SelectedMods.Value = new[] { ruleset.CreateMod() }; Player = CreatePlayer(ruleset); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index b536233ff0..cc64d37116 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -15,6 +15,7 @@ using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.Play; @@ -388,6 +389,19 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden); } + [Test] + public void TestExitGameFromSongSelect() + { + PushAndConfirm(() => new TestPlaySongSelect()); + exitViaEscapeAndConfirm(); + + pushEscape(); // returns to osu! logo + + AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroTriangles); + AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); + } + private void pushEscape() => AddStep("Press escape", () => InputManager.Key(Key.Escape)); diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 5bfb676f81..963809ebe1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -5,10 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -17,10 +17,11 @@ using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Users; +using osuTK.Input; namespace osu.Game.Tests.Visual.Online { - public class TestSceneBeatmapListingOverlay : OsuTestScene + public class TestSceneBeatmapListingOverlay : OsuManualInputManagerTestScene { private readonly List setsForResponse = new List(); @@ -28,27 +29,33 @@ namespace osu.Game.Tests.Visual.Online private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType().Single(); - [BackgroundDependencyLoader] - private void load() + [SetUpSteps] + public void SetUpSteps() { - Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } }; - - ((DummyAPIAccess)API).HandleRequest = req => + AddStep("setup overlay", () => { - if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false; - - searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse - { - BeatmapSets = setsForResponse, - }); - - return true; - }; + Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } }; + setsForResponse.Clear(); + }); AddStep("initialize dummy", () => { + var api = (DummyAPIAccess)API; + + api.HandleRequest = req => + { + if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false; + + searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse + { + BeatmapSets = setsForResponse, + }); + + return true; + }; + // non-supporter user - ((DummyAPIAccess)API).LocalUser.Value = new User + api.LocalUser.Value = new User { Username = "TestBot", Id = API.LocalUser.Value.Id + 1, @@ -56,6 +63,51 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestHideViaBack() + { + AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); + AddStep("hide", () => InputManager.Key(Key.Escape)); + AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden); + } + + [Test] + public void TestHideViaBackWithSearch() + { + AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); + + AddStep("search something", () => overlay.ChildrenOfType().First().Text = "search"); + + AddStep("kill search", () => InputManager.Key(Key.Escape)); + + AddAssert("search textbox empty", () => string.IsNullOrEmpty(overlay.ChildrenOfType().First().Text)); + AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); + + AddStep("hide", () => InputManager.Key(Key.Escape)); + AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden); + } + + [Test] + public void TestHideViaBackWithScrolledSearch() + { + AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); + + AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, 100).ToArray())); + + AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any(d => d.IsPresent)); + + AddStep("scroll to bottom", () => overlay.ChildrenOfType().First().ScrollToEnd()); + + AddStep("kill search", () => InputManager.Key(Key.Escape)); + + AddUntilStep("search textbox empty", () => string.IsNullOrEmpty(overlay.ChildrenOfType().First().Text)); + AddUntilStep("is scrolled to top", () => overlay.ChildrenOfType().First().Current == 0); + AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); + + AddStep("hide", () => InputManager.Key(Key.Escape)); + AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden); + } + [Test] public void TestNoBeatmapsPlaceholder() { @@ -63,7 +115,7 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); - AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any()); + AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any(d => d.IsPresent)); AddStep("fetch for 0 beatmaps", () => fetchFor()); AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); @@ -193,13 +245,15 @@ namespace osu.Game.Tests.Visual.Online noPlaceholderShown(); } + private static int searchCount; + private void fetchFor(params BeatmapSetInfo[] beatmaps) { setsForResponse.Clear(); setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b))); // trigger arbitrary change for fetching. - searchControl.Query.TriggerChange(); + searchControl.Query.Value = $"search {searchCount++}"; } private void setRankAchievedFilter(ScoreRank[] ranks) @@ -229,8 +283,8 @@ namespace osu.Game.Tests.Visual.Online private void noPlaceholderShown() { AddUntilStep("no placeholder shown", () => - !overlay.ChildrenOfType().Any() - && !overlay.ChildrenOfType().Any()); + !overlay.ChildrenOfType().Any(d => d.IsPresent) + && !overlay.ChildrenOfType().Any(d => d.IsPresent)); } private class TestAPIBeatmapSet : APIBeatmapSet diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index edc1696456..f420ad976b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -21,6 +21,8 @@ namespace osu.Game.Tests.Visual.Online protected override bool UseOnlineAPI => true; + private int nextBeatmapSetId = 1; + public TestSceneBeatmapSetOverlay() { Add(overlay = new TestBeatmapSetOverlay()); @@ -240,12 +242,23 @@ namespace osu.Game.Tests.Visual.Online { AddStep("show explicit map", () => { - var beatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; + var beatmapSet = getBeatmapSet(); beatmapSet.OnlineInfo.HasExplicitContent = true; overlay.ShowBeatmapSet(beatmapSet); }); } + [Test] + public void TestFeaturedBeatmap() + { + AddStep("show featured map", () => + { + var beatmapSet = getBeatmapSet(); + beatmapSet.OnlineInfo.TrackId = 1; + overlay.ShowBeatmapSet(beatmapSet); + }); + } + [Test] public void TestHide() { @@ -308,6 +321,14 @@ namespace osu.Game.Tests.Visual.Online }; } + private BeatmapSetInfo getBeatmapSet() + { + var beatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; + // Make sure the overlay is reloaded (see `BeatmapSetInfo.Equals`). + beatmapSet.OnlineBeatmapSetID = nextBeatmapSetId++; + return beatmapSet; + } + private void downloadAssert(bool shown) { AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown); diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index fd5f306e07..722010ace2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -99,16 +99,23 @@ namespace osu.Game.Tests.Visual.Online [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - var normal = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; + var normal = getBeatmapSet(); normal.OnlineInfo.HasVideo = true; normal.OnlineInfo.HasStoryboard = true; var undownloadable = getUndownloadableBeatmapSet(); var manyDifficulties = getManyDifficultiesBeatmapSet(rulesets); - var explicitMap = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; + var explicitMap = getBeatmapSet(); explicitMap.OnlineInfo.HasExplicitContent = true; + var featuredMap = getBeatmapSet(); + featuredMap.OnlineInfo.TrackId = 1; + + var explicitFeaturedMap = getBeatmapSet(); + explicitFeaturedMap.OnlineInfo.HasExplicitContent = true; + explicitFeaturedMap.OnlineInfo.TrackId = 2; + Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, @@ -125,13 +132,19 @@ namespace osu.Game.Tests.Visual.Online new GridBeatmapPanel(undownloadable), new GridBeatmapPanel(manyDifficulties), new GridBeatmapPanel(explicitMap), + new GridBeatmapPanel(featuredMap), + new GridBeatmapPanel(explicitFeaturedMap), new ListBeatmapPanel(normal), new ListBeatmapPanel(undownloadable), new ListBeatmapPanel(manyDifficulties), - new ListBeatmapPanel(explicitMap) + new ListBeatmapPanel(explicitMap), + new ListBeatmapPanel(featuredMap), + new ListBeatmapPanel(explicitFeaturedMap) }, }, }; + + BeatmapSetInfo getBeatmapSet() => CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index 40b2f66d74..dcc2111ad3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select EZ mod", () => { var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); - SelectedMods.Value = new[] { ruleset.GetAllMods().OfType().Single() }; + SelectedMods.Value = new[] { ruleset.CreateMod() }; }); AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue)); @@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select HR mod", () => { var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); - SelectedMods.Value = new[] { ruleset.GetAllMods().OfType().Single() }; + SelectedMods.Value = new[] { ruleset.CreateMod() }; }); AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue)); @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select unchanged Difficulty Adjust mod", () => { var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); - var difficultyAdjustMod = ruleset.GetAllMods().OfType().Single(); + var difficultyAdjustMod = ruleset.CreateMod(); difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty); SelectedMods.Value = new[] { difficultyAdjustMod }; }); @@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select changed Difficulty Adjust mod", () => { var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); - var difficultyAdjustMod = ruleset.GetAllMods().OfType().Single(); + var difficultyAdjustMod = ruleset.CreateMod(); var originalDifficulty = advancedStats.Beatmap.BaseDifficulty; difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs index 66ac700c51..f91d3f595b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("setup display", () => { - var randomMods = Ruleset.Value.CreateInstance().GetAllMods().OrderBy(_ => RNG.Next()).Take(5).ToList(); + var randomMods = Ruleset.Value.CreateInstance().CreateAllMods().OrderBy(_ => RNG.Next()).Take(5).ToList(); OsuLogo logo = new OsuLogo { Scale = new Vector2(0.15f) }; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModFlowDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModFlowDisplay.cs index 8f057c663b..10eab148de 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModFlowDisplay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModFlowDisplay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface Width = 200, Current = { - Value = new OsuRuleset().GetAllMods().ToArray(), + Value = new OsuRuleset().CreateAllMods().ToArray(), } }; }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs index e7fa7d9235..513eb2fafc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; @@ -17,5 +19,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime())); AddStep("change mod", () => icon.Mod = new OsuModEasy()); } + + [Test] + public void TestInterfaceModType() + { + ModIcon icon = null; + + var ruleset = new OsuRuleset(); + + AddStep("create mod icon", () => Child = icon = new ModIcon(ruleset.AllMods.First(m => m.Acronym == "DT"))); + AddStep("change mod", () => icon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ")); + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 9e253e089d..4f7aec3b67 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -158,8 +158,8 @@ namespace osu.Game.Tests.Visual.UserInterface var mania = new ManiaRuleset(); testModsWithSameBaseType( - mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)), - mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden))); + mania.CreateMod(), + mania.CreateMod()); } [Test] diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index b4d9fa4222..47e7ed9b61 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap apiBeatmap) { beatmap = apiBeatmap.ToBeatmap(rulesets); - var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods(); + var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().AllMods; foreach (var mod in mods) { diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs index 43ac92d285..7c4e9c69a2 100644 --- a/osu.Game.Tournament/Components/TournamentModIcon.cs +++ b/osu.Game.Tournament/Components/TournamentModIcon.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -49,7 +48,7 @@ namespace osu.Game.Tournament.Components } var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0); - var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == modAcronym); + var modIcon = ruleset?.CreateInstance().CreateModFromAcronym(modAcronym); if (modIcon == null) return; diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs index 48f1f0ce68..3658dbab83 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs @@ -90,6 +90,12 @@ namespace osu.Game.Beatmaps /// The song language of this beatmap set. /// public BeatmapSetOnlineLanguage Language { get; set; } + + /// + /// The track ID of this beatmap set. + /// Non-null only if the track is linked to a featured artist track entry. + /// + public int? TrackId { get; set; } } public class BeatmapSetOnlineGenre diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index ed9f0710b0..6c8238a1b8 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterface return base.OnKeyDown(e); } - public bool OnPressed(GlobalAction action) + public virtual bool OnPressed(GlobalAction action) { if (!HasFocus) return false; diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 596d567480..62f9976c0f 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -16,7 +16,7 @@ using osu.Game.Utils; namespace osu.Game.Online.API { [MessagePackObject] - public class APIMod : IMod, IEquatable + public class APIMod : IEquatable { [JsonProperty("acronym")] [Key(0)] @@ -48,7 +48,7 @@ namespace osu.Game.Online.API public Mod ToMod(Ruleset ruleset) { - Mod resultMod = ruleset.GetAllMods().FirstOrDefault(m => m.Acronym == Acronym); + Mod resultMod = ruleset.CreateModFromAcronym(Acronym); if (resultMod == null) throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}."); @@ -67,15 +67,12 @@ namespace osu.Game.Online.API return resultMod; } - public bool Equals(IMod other) => other is APIMod them && Equals(them); - public bool Equals(APIMod other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return Acronym == other.Acronym && - Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default); + return Acronym == other.Acronym && Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default); } public override string ToString() diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index bf3441d2a0..b4e0e44b2c 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -18,9 +18,9 @@ namespace osu.Game.Online.API.Requests private readonly BeatmapInfo beatmap; private readonly BeatmapLeaderboardScope scope; private readonly RulesetInfo ruleset; - private readonly IEnumerable mods; + private readonly IEnumerable mods; - public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null) + public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null) { if (!beatmap.OnlineBeatmapID.HasValue) throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}."); @@ -31,7 +31,7 @@ namespace osu.Game.Online.API.Requests this.beatmap = beatmap; this.scope = scope; this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset)); - this.mods = mods ?? Array.Empty(); + this.mods = mods ?? Array.Empty(); Success += onSuccess; } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 45d9c9405f..f653a654ca 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -63,6 +63,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"ratings")] private int[] ratings { get; set; } + [JsonProperty(@"track_id")] + private int? trackId { get; set; } + [JsonProperty(@"user_id")] private int creatorId { @@ -106,7 +109,8 @@ namespace osu.Game.Online.API.Requests.Responses Availability = availability, HasFavourited = hasFavourited, Genre = genre, - Language = language + Language = language, + TrackId = trackId }, }; diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 1b394185fd..567df524b1 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -23,10 +23,10 @@ namespace osu.Game.Online.API.Requests.Responses var rulesetInstance = ruleset.CreateInstance(); - var mods = Mods != null ? rulesetInstance.GetAllMods().Where(mod => Mods.Contains(mod.Acronym)).ToArray() : Array.Empty(); + var mods = Mods != null ? Mods.Select(acronym => rulesetInstance.CreateModFromAcronym(acronym)).Where(m => m != null).ToArray() : Array.Empty(); // all API scores provided by this class are considered to be legacy. - mods = mods.Append(rulesetInstance.GetAllMods().OfType().Single()).ToArray(); + mods = mods.Append(rulesetInstance.CreateMod()).ToArray(); var scoreInfo = new ScoreInfo { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index d1e2ac38df..07a0cfb57f 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -3,21 +3,22 @@ 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 osuTK; -using osu.Framework.Bindables; using osu.Framework.Input.Events; -using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; -using osuTK.Graphics; using osu.Game.Rulesets; using osu.Game.Scoring; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.BeatmapListing { @@ -117,7 +118,7 @@ namespace osu.Game.Overlays.BeatmapListing textBox = new BeatmapSearchTextBox { RelativeSizeAxes = Axes.X, - TypingStarted = () => TypingStarted?.Invoke(), + TextChanged = () => TypingStarted?.Invoke(), }, new ReverseChildIDFillFlowContainer { @@ -167,7 +168,7 @@ namespace osu.Game.Overlays.BeatmapListing /// /// Any time the text box receives key events (even while masked). /// - public Action TypingStarted; + public Action TextChanged; protected override Color4 SelectionColour => Color4.Gray; @@ -181,7 +182,16 @@ namespace osu.Game.Overlays.BeatmapListing if (!base.OnKeyDown(e)) return false; - TypingStarted?.Invoke(); + TextChanged?.Invoke(); + return true; + } + + public override bool OnPressed(GlobalAction action) + { + if (!base.OnPressed(action)) + return false; + + TextChanged?.Invoke(); return true; } } diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index 4d5c387c4a..c078127353 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels private const float horizontal_padding = 10; private const float vertical_padding = 5; - private FillFlowContainer bottomPanel, statusContainer, titleContainer; + private FillFlowContainer bottomPanel, statusContainer, titleContainer, artistContainer; private PlayButton playButton; private Box progressBar; @@ -89,11 +89,19 @@ namespace osu.Game.Overlays.BeatmapListing.Panels }, } }, - new OsuSpriteText + artistContainer = new FillFlowContainer { - Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), - Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) - }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), + Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) + } + } + } }, }, new Container @@ -213,6 +221,16 @@ namespace osu.Game.Overlays.BeatmapListing.Panels }); } + if (SetInfo.OnlineInfo?.TrackId != null) + { + artistContainer.Add(new FeaturedArtistBeatmapPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 10f, Top = 2f }, + }); + } + if (SetInfo.OnlineInfo?.HasVideo ?? false) { statusContainer.Add(new IconPill(FontAwesome.Solid.Film)); diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs index 00ffd168c1..5011749c5f 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels private const float vertical_padding = 5; private const float height = 70; - private FillFlowContainer statusContainer, titleContainer; + private FillFlowContainer statusContainer, titleContainer, artistContainer; protected BeatmapPanelDownloadButton DownloadButton; private PlayButton playButton; private Box progressBar; @@ -112,10 +112,18 @@ namespace osu.Game.Overlays.BeatmapListing.Panels }, } }, - new OsuSpriteText + artistContainer = new FillFlowContainer { - Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), - Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + new OsuSpriteText + { + Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), + Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) + }, + }, }, } }, @@ -227,6 +235,16 @@ namespace osu.Game.Overlays.BeatmapListing.Panels }); } + if (SetInfo.OnlineInfo?.TrackId != null) + { + artistContainer.Add(new FeaturedArtistBeatmapPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 10f, Top = 2f }, + }); + } + if (SetInfo.OnlineInfo?.HasVideo ?? false) { statusContainer.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index a61640a02e..c3b6444a24 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -37,6 +37,7 @@ namespace osu.Game.Overlays.BeatmapSet private readonly OsuSpriteText title, artist; private readonly AuthorInfo author; private readonly ExplicitContentBeatmapPill explicitContentPill; + private readonly FeaturedArtistBeatmapPill featuredArtistPill; private readonly FillFlowContainer downloadButtonsContainer; private readonly BeatmapAvailability beatmapAvailability; private readonly BeatmapSetOnlineStatusPill onlineStatusPill; @@ -129,10 +130,25 @@ namespace osu.Game.Overlays.BeatmapSet } } }, - artist = new OsuSpriteText + new FillFlowContainer { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true), - Margin = new MarginPadding { Bottom = 20 } + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Bottom = 20 }, + Children = new Drawable[] + { + artist = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true), + }, + featuredArtistPill = new FeaturedArtistBeatmapPill + { + Alpha = 0f, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 10 } + } + } }, new Container { @@ -233,6 +249,7 @@ namespace osu.Game.Overlays.BeatmapSet artist.Text = new RomanisableString(setInfo.NewValue.Metadata.ArtistUnicode, setInfo.NewValue.Metadata.Artist); explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; + featuredArtistPill.Alpha = setInfo.NewValue.OnlineInfo.TrackId != null ? 1 : 0; onlineStatusPill.FadeIn(500, Easing.OutQuint); onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; diff --git a/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapPill.cs b/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapPill.cs new file mode 100644 index 0000000000..fdee0799ff --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapPill.cs @@ -0,0 +1,47 @@ +// 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.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Overlays.BeatmapSet +{ + public class FeaturedArtistBeatmapPill : CompositeDrawable + { + public FeaturedArtistBeatmapPill() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, OverlayColourProvider colourProvider) + { + InternalChild = new CircularContainer + { + Masking = true, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider?.Background5 ?? colours.Gray2, + }, + new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f }, + Text = BeatmapsetsStrings.FeaturedArtistBadgeLabel.ToUpper(), + Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), + Colour = OverlayColourProvider.Blue.Colour1, + } + } + }; + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs index 5b903372fd..6349f115cb 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardModSelector.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.BeatmapSet { public class LeaderboardModSelector : CompositeDrawable { - public readonly BindableList SelectedMods = new BindableList(); + public readonly BindableList SelectedMods = new BindableList(); public readonly Bindable Ruleset = new Bindable(); private readonly FillFlowContainer modsContainer; @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.BeatmapSet return; modsContainer.Add(new ModButton(new ModNoMod())); - modsContainer.AddRange(ruleset.NewValue.CreateInstance().GetAllMods().Where(m => m.UserPlayable).Select(m => new ModButton(m))); + modsContainer.AddRange(ruleset.NewValue.CreateInstance().AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m))); modsContainer.ForEach(button => { @@ -76,7 +76,7 @@ namespace osu.Game.Overlays.BeatmapSet updateHighlighted(); } - private void selectionChanged(Mod mod, bool selected) + private void selectionChanged(IMod mod, bool selected) { if (selected) SelectedMods.Add(mod); @@ -101,9 +101,9 @@ namespace osu.Game.Overlays.BeatmapSet private const int duration = 200; public readonly BindableBool Highlighted = new BindableBool(); - public Action OnSelectionChanged; + public Action OnSelectionChanged; - public ModButton(Mod mod) + public ModButton(IMod mod) : base(mod) { Scale = new Vector2(0.4f); diff --git a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs index c2b855a0f8..1b0a62dc4a 100644 --- a/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs +++ b/osu.Game/Overlays/Changelog/ChangelogSupporterPromo.cs @@ -26,9 +26,6 @@ namespace osu.Game.Overlays.Changelog private const float image_container_width = 164; private const float heart_size = 75; - private readonly FillFlowContainer textContainer; - private readonly Container imageContainer; - public ChangelogSupporterPromo() { RelativeSizeAxes = Axes.X; @@ -38,6 +35,12 @@ namespace osu.Game.Overlays.Changelog Vertical = 20, Horizontal = 50, }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour, TextureStore textures, OverlayColourProvider colourProvider) + { + SupporterPromoLinkFlowContainer supportLinkText; InternalChildren = new Drawable[] { @@ -59,7 +62,7 @@ namespace osu.Game.Overlays.Changelog new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.3f), + Colour = colourProvider.Background5, }, new Container { @@ -68,7 +71,7 @@ namespace osu.Game.Overlays.Changelog Padding = new MarginPadding { Horizontal = 75 }, Children = new Drawable[] { - textContainer = new FillFlowContainer + new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -76,91 +79,84 @@ namespace osu.Game.Overlays.Changelog Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding { Right = 50 + image_container_width }, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = ChangelogStrings.SupportHeading, + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light), + Margin = new MarginPadding { Bottom = 20 }, + }, + supportLinkText = new SupporterPromoLinkFlowContainer(t => + { + t.Font = t.Font.With(size: 14); + t.Colour = colour.PinkLighter; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + new OsuTextFlowContainer(t => + { + t.Font = t.Font.With(size: 12); + t.Colour = colour.PinkLighter; + }) + { + Text = ChangelogStrings.SupportText2.ToString(), + Margin = new MarginPadding { Top = 10 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + }, }, - imageContainer = new Container + new Container { RelativeSizeAxes = Axes.Y, Width = image_container_width, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + Children = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 28 }, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + Texture = textures.Get(@"Online/supporter-pippi"), + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(heart_size), + Margin = new MarginPadding { Top = 70 }, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = colour.Pink, + Radius = 10, + Roundness = heart_size / 2, + }, + Child = new Sprite + { + Size = new Vector2(heart_size), + Texture = textures.Get(@"Online/supporter-heart"), + }, + }, + } } } }, } }, }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colour, TextureStore textures) - { - SupporterPromoLinkFlowContainer supportLinkText; - textContainer.Children = new Drawable[] - { - new OsuSpriteText - { - Text = ChangelogStrings.SupportHeading, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light), - Margin = new MarginPadding { Bottom = 20 }, - }, - supportLinkText = new SupporterPromoLinkFlowContainer(t => - { - t.Font = t.Font.With(size: 14); - t.Colour = colour.PinkLighter; - }) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - new OsuTextFlowContainer(t => - { - t.Font = t.Font.With(size: 12); - t.Colour = colour.PinkLighter; - }) - { - Text = ChangelogStrings.SupportText2.ToString(), - Margin = new MarginPadding { Top = 10 }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - }; supportLinkText.AddText("Support further development of osu! and "); supportLinkText.AddLink("become an osu!supporter", @"https://osu.ppy.sh/home/support", t => t.Font = t.Font.With(weight: FontWeight.Bold)); supportLinkText.AddText(" today!"); - - imageContainer.Children = new Drawable[] - { - new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Margin = new MarginPadding { Bottom = 28 }, - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, - Texture = textures.Get(@"Online/supporter-pippi"), - }, - new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(heart_size), - Margin = new MarginPadding { Top = 70 }, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = colour.Pink, - Radius = 10, - Roundness = heart_size / 2, - }, - Child = new Sprite - { - Size = new Vector2(heart_size), - Texture = textures.Get(@"Online/supporter-heart"), - }, - }, - }; } private class SupporterPromoLinkFlowContainer : LinkFlowContainer diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs index c8e44ee159..0f51439252 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModButton.cs @@ -107,9 +107,9 @@ namespace osu.Game.Overlays.Mods var incompatibleTypes = mod.IncompatibleMods; - var allMods = ruleset.Value.CreateInstance().GetAllMods(); + var allMods = ruleset.Value.CreateInstance().AllMods; - incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).ToList(); + incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).Select(m => m.CreateInstance()).ToList(); incompatibleText.Text = incompatibleMods.Value.Any() ? "Incompatible with:" : "Compatible with all mods"; } } diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index a5e19f293c..d5d1de91de 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using Newtonsoft.Json; +using osu.Framework.Graphics.Sprites; namespace osu.Game.Rulesets.Mods { @@ -11,7 +11,37 @@ namespace osu.Game.Rulesets.Mods /// /// The shortened name of this mod. /// - [JsonProperty("acronym")] string Acronym { get; } + + /// + /// The name of this mod. + /// + string Name { get; } + + /// + /// The user readable description of this mod. + /// + string Description { get; } + + /// + /// The type of this mod. + /// + ModType Type { get; } + + /// + /// The icon of this mod. + /// + IconUsage? Icon { get; } + + /// + /// Whether this mod is playable by an end user. + /// Should be false for cases where the user is not interacting with the game (so it can be excluded from multiplayer selection, for example). + /// + bool UserPlayable { get; } + + /// + /// Create a fresh instance based on this mod. + /// + Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType()); } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 1199d8a956..7136795461 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -22,32 +22,17 @@ namespace osu.Game.Rulesets.Mods [ExcludeFromDynamicCompile] public abstract class Mod : IMod, IEquatable, IDeepCloneable { - /// - /// The name of this mod. - /// [JsonIgnore] public abstract string Name { get; } - /// - /// The shortened name of this mod. - /// public abstract string Acronym { get; } - /// - /// The icon of this mod. - /// [JsonIgnore] public virtual IconUsage? Icon => null; - /// - /// The type of this mod. - /// [JsonIgnore] public virtual ModType Type => ModType.Fun; - /// - /// The user readable description of this mod. - /// [JsonIgnore] public abstract string Description { get; } @@ -106,10 +91,6 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool HasImplementation => this is IApplicableMod; - /// - /// Whether this mod is playable by an end user. - /// Should be false for cases where the user is not interacting with the game (so it can be excluded from mutliplayer selection, for example). - /// [JsonIgnore] public virtual bool UserPlayable => true; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index de62cf8d33..b0c3836774 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; @@ -38,13 +39,58 @@ namespace osu.Game.Rulesets { public RulesetInfo RulesetInfo { get; internal set; } - public IEnumerable GetAllMods() => Enum.GetValues(typeof(ModType)).Cast() - // Confine all mods of each mod type into a single IEnumerable - .SelectMany(GetModsFor) - // Filter out all null mods - .Where(mod => mod != null) - // Resolve MultiMods as their .Mods property - .SelectMany(mod => (mod as MultiMod)?.Mods ?? new[] { mod }); + private static readonly ConcurrentDictionary mod_reference_cache = new ConcurrentDictionary(); + + /// + /// A queryable source containing all available mods. + /// Call for consumption purposes. + /// + public IEnumerable AllMods + { + get + { + if (!(RulesetInfo.ID is int id)) + return CreateAllMods(); + + if (!mod_reference_cache.TryGetValue(id, out var mods)) + mod_reference_cache[id] = mods = CreateAllMods().Cast().ToArray(); + + return mods; + } + } + + /// + /// Returns fresh instances of all mods. + /// + /// + /// This comes with considerable allocation overhead. If only accessing for reference purposes (ie. not changing bindables / settings) + /// use instead. + /// + public IEnumerable CreateAllMods() => Enum.GetValues(typeof(ModType)).Cast() + // Confine all mods of each mod type into a single IEnumerable + .SelectMany(GetModsFor) + // Filter out all null mods + .Where(mod => mod != null) + // Resolve MultiMods as their .Mods property + .SelectMany(mod => (mod as MultiMod)?.Mods ?? new[] { mod }); + + /// + /// Returns a fresh instance of the mod matching the specified acronym. + /// + /// The acronym to query for . + public Mod CreateModFromAcronym(string acronym) + { + return AllMods.FirstOrDefault(m => m.Acronym == acronym)?.CreateInstance(); + } + + /// + /// Returns a fresh instance of the mod matching the specified type. + /// + public T CreateMod() + where T : Mod + { + return AllMods.FirstOrDefault(m => m is T)?.CreateInstance() as T; + } public abstract IEnumerable GetModsFor(ModType type); @@ -126,7 +172,7 @@ namespace osu.Game.Rulesets } [CanBeNull] - public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().FirstOrDefault(); + public ModAutoplay GetAutoplayMod() => CreateMod(); public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null; diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 725cfa9c26..79bada0490 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -30,12 +30,12 @@ namespace osu.Game.Rulesets.UI private const float size = 80; - public virtual LocalisableString TooltipText => showTooltip ? mod.IconTooltip : null; + public virtual LocalisableString TooltipText => showTooltip ? ((mod as Mod)?.IconTooltip ?? mod.Name) : null; - private Mod mod; + private IMod mod; private readonly bool showTooltip; - public Mod Mod + public IMod Mod { get => mod; set @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.UI /// /// The mod to be displayed /// Whether a tooltip describing the mod should display on hover. - public ModIcon(Mod mod, bool showTooltip = true) + public ModIcon(IMod mod, bool showTooltip = true) { this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); this.showTooltip = showTooltip; @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI updateMod(mod); } - private void updateMod(Mod value) + private void updateMod(IMod value) { modAcronym.Text = value.Acronym; modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 2f17167297..2e1a29372d 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -67,7 +67,7 @@ namespace osu.Game.Scoring.Legacy // lazer replays get a really high version number. if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION) - scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.GetAllMods().OfType().Single()).ToArray(); + scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.CreateMod()).ToArray(); currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 07a94fb97e..cfe14eab92 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -115,7 +115,9 @@ namespace osu.Game.Screens.Menu if (setInfo == null) return false; - return (initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0])) != null; + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); + + return UsingThemedIntro = initialBeatmap != null; } } @@ -165,7 +167,7 @@ namespace osu.Game.Screens.Menu protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack(); - protected void StartTrack() + protected virtual void StartTrack() { // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. if (UsingThemedIntro) @@ -184,7 +186,6 @@ namespace osu.Game.Screens.Menu { beatmap.Value = initialBeatmap; Track = initialBeatmap.Track; - UsingThemedIntro = !initialBeatmap.Track.IsDummyDevice; // ensure the track starts at maximum volume musicController.CurrentTrack.FinishTransforms(); diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 0ea83fe5e7..a8ca17cec1 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -41,6 +41,9 @@ namespace osu.Game.Screens.Menu private Sample welcome; + private DecoupleableInterpolatingFramedClock decoupledClock; + private TrianglesIntroSequence intro; + [BackgroundDependencyLoader] private void load() { @@ -56,10 +59,18 @@ namespace osu.Game.Screens.Menu { PrepareMenuLoad(); - LoadComponentAsync(new TrianglesIntroSequence(logo, background) + decoupledClock = new DecoupleableInterpolatingFramedClock + { + IsCoupled = false + }; + + if (UsingThemedIntro) + decoupledClock.ChangeSource(Track); + + LoadComponentAsync(intro = new TrianglesIntroSequence(logo, background) { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(UsingThemedIntro ? Track : null), + Clock = decoupledClock, LoadMenu = LoadMenu }, t => { @@ -72,12 +83,25 @@ namespace osu.Game.Screens.Menu } } + public override void OnSuspending(IScreen next) + { + base.OnSuspending(next); + + // 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(IScreen last) { base.OnResuming(last); background.FadeOut(100); } + protected override void StartTrack() + { + decoupledClock.Start(); + } + private class TrianglesIntroSequence : CompositeDrawable { private readonly OsuLogo logo; diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index 54a83f4305..b7803f3420 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps protected void TestToLegacy(LegacyMods expectedLegacyMods, Type[] providedModTypes) { var ruleset = CreateRuleset(); - var modInstances = ruleset.GetAllMods() + var modInstances = ruleset.CreateAllMods() .Where(mod => providedModTypes.Contains(mod.GetType())) .ToArray(); var actualLegacyMods = ruleset.ConvertToLegacyMods(modInstances); diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 8ce71ace69..5ce6aae647 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests RulesetID = ruleset.ID ?? 0; Mods = excessMods - ? ruleset.CreateInstance().GetAllMods().ToArray() + ? ruleset.CreateInstance().CreateAllMods().ToArray() : new Mod[] { new TestModHardRock(), new TestModDoubleTime() }; TotalScore = 2845370; diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 93491c800f..b34f7e2d5f 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual if (!AllowFail) { - var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail); + var noFailMod = ruleset.CreateMod(); if (noFailMod != null) SelectedMods.Value = new[] { noFailMod }; }