diff --git a/UseLocalFramework.ps1 b/UseLocalFramework.ps1 index 837685f310..9f4547d980 100644 --- a/UseLocalFramework.ps1 +++ b/UseLocalFramework.ps1 @@ -3,15 +3,53 @@ # # https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects -$CSPROJ="osu.Game/osu.Game.csproj" +$GAME_CSPROJ="osu.Game/osu.Game.csproj" +$ANDROID_PROPS="osu.Android.props" +$IOS_PROPS="osu.iOS.props" $SLN="osu.sln" -dotnet remove $CSPROJ package ppy.osu.Framework; -dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj; -dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj +dotnet remove $GAME_CSPROJ reference ppy.osu.Framework; +dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android; +dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS; + +dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ` + ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj ` + ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj ` + ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj; + +dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj; +dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj; +dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj; + +# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files +(Get-Content "osu.Android.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.Android.props" +(Get-Content "osu.iOS.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.iOS.props" + +# needed because iOS framework nupkg includes a set of properties to work around certain issues during building, +# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference. +(Get-Content "osu.iOS.props") | + Foreach-Object { + if ($_ -match "") + { + " " + } + + $_ + } | Set-Content "osu.iOS.props" + +$TMP=New-TemporaryFile $SLNF=Get-Content "osu.Desktop.slnf" | ConvertFrom-Json -$TMP=New-TemporaryFile $SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj") ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8 Move-Item -Path $TMP -Destination "osu.Desktop.slnf" -Force + +$SLNF=Get-Content "osu.Android.slnf" | ConvertFrom-Json +$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj") +ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8 +Move-Item -Path $TMP -Destination "osu.Android.slnf" -Force + +$SLNF=Get-Content "osu.iOS.slnf" | ConvertFrom-Json +$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj") +ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8 +Move-Item -Path $TMP -Destination "osu.iOS.slnf" -Force diff --git a/UseLocalFramework.sh b/UseLocalFramework.sh index 4fd1fdfd1b..c12b388e96 100755 --- a/UseLocalFramework.sh +++ b/UseLocalFramework.sh @@ -5,14 +5,41 @@ # # https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects -CSPROJ="osu.Game/osu.Game.csproj" +GAME_CSPROJ="osu.Game/osu.Game.csproj" +ANDROID_PROPS="osu.Android.props" +IOS_PROPS="osu.iOS.props" SLN="osu.sln" -dotnet remove $CSPROJ package ppy.osu.Framework -dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj -dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj +dotnet remove $GAME_CSPROJ reference ppy.osu.Framework +dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android +dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS + +dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj \ + ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj \ + ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj \ + ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj + +dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj +dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj +dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj + +# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files +sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.Android.props && rm osu.Android.props.bak +sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.iOS.props && rm osu.iOS.props.bak + +# needed because iOS framework nupkg includes a set of properties to work around certain issues during building, +# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference. +sed -i.bak '/<\/Project>/i\ + \ +' ./osu.iOS.props && rm osu.iOS.props.bak -SLNF="osu.Desktop.slnf" tmp=$(mktemp) + jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj"]' osu.Desktop.slnf > $tmp -mv -f $tmp $SLNF +mv -f $tmp osu.Desktop.slnf + +jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj"]' osu.Android.slnf > $tmp +mv -f $tmp osu.Android.slnf + +jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj"]' osu.iOS.slnf > $tmp +mv -f $tmp osu.iOS.slnf diff --git a/osu.Android.props b/osu.Android.props index abd8406cf7..234575fc62 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -8,10 +8,14 @@ true true + manifestmerger.jar + + + diff --git a/osu.Android/Properties/AndroidManifestOverlay.xml b/osu.Android/Properties/AndroidManifestOverlay.xml new file mode 100644 index 0000000000..815f935383 --- /dev/null +++ b/osu.Android/Properties/AndroidManifestOverlay.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs b/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs new file mode 100644 index 0000000000..8f7027da17 --- /dev/null +++ b/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using BenchmarkDotNet.Attributes; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; + +namespace osu.Game.Benchmarks +{ + public class BenchmarkCarouselFilter : BenchmarkTest + { + private BeatmapInfo getExampleBeatmap() => new BeatmapInfo + { + Ruleset = new RulesetInfo + { + ShortName = "osu", + OnlineID = 0 + }, + StarRating = 4.0d, + Difficulty = new BeatmapDifficulty + { + ApproachRate = 5.0f, + DrainRate = 3.0f, + CircleSize = 2.0f, + }, + Metadata = new BeatmapMetadata + { + Artist = "The Artist", + ArtistUnicode = "check unicode too", + Title = "Title goes here", + TitleUnicode = "Title goes here", + Author = { Username = "The Author" }, + Source = "unit tests", + Tags = "look for tags too", + }, + DifficultyName = "version as well", + Length = 2500, + BPM = 160, + BeatDivisor = 12, + Status = BeatmapOnlineStatus.Loved + }; + + private CarouselBeatmap carouselBeatmap = null!; + private FilterCriteria criteria1 = null!; + private FilterCriteria criteria2 = null!; + private FilterCriteria criteria3 = null!; + private FilterCriteria criteria4 = null!; + private FilterCriteria criteria5 = null!; + private FilterCriteria criteria6 = null!; + + public override void SetUp() + { + var beatmap = getExampleBeatmap(); + beatmap.OnlineID = 20201010; + beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 }; + carouselBeatmap = new CarouselBeatmap(beatmap); + criteria1 = new FilterCriteria(); + criteria2 = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "catch" } + }; + criteria3 = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = 6 }, + AllowConvertedBeatmaps = true, + BPM = new FilterCriteria.OptionalRange + { + IsUpperInclusive = false, + Max = 160d + } + }; + criteria4 = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = 6 }, + AllowConvertedBeatmaps = true, + SearchText = "an artist" + }; + criteria5 = new FilterCriteria + { + Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = "the author AND then something else" } + }; + criteria6 = new FilterCriteria { SearchText = "20201010" }; + } + + [Benchmark] + public void CarouselBeatmapFilter() + { + carouselBeatmap.Filter(criteria1); + } + + [Benchmark] + public void CriteriaMatchingSpecificRuleset() + { + carouselBeatmap.Filter(criteria2); + } + + [Benchmark] + public void CriteriaMatchingRangeMax() + { + carouselBeatmap.Filter(criteria3); + } + + [Benchmark] + public void CriteriaMatchingTerms() + { + carouselBeatmap.Filter(criteria4); + } + + [Benchmark] + public void CriteriaMatchingCreator() + { + carouselBeatmap.Filter(criteria5); + } + + [Benchmark] + public void CriteriaMatchingBeatmapIDs() + { + carouselBeatmap.Filter(criteria6); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 69eacda541..ef4810c40d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -167,8 +167,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { if (bodySprite != null) { - bodySprite.Origin = Anchor.BottomCentre; - bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y) * -1); + bodySprite.Origin = Anchor.TopCentre; + bodySprite.Anchor = Anchor.BottomCentre; // needs to be flipped due to scale flip in Update. } if (light != null) @@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy if (bodySprite != null) { bodySprite.Origin = Anchor.TopCentre; - bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y)); + bodySprite.Anchor = Anchor.TopCentre; } if (light != null) @@ -211,11 +211,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy base.Update(); missFadeTime.Value ??= holdNote.HoldBrokenTime; + int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1); + // here we go... switch (bodyStyle) { case LegacyNoteBodyStyle.Stretch: // this is how lazer works by default. nothing required. + if (bodySprite != null) + bodySprite.Scale = new Vector2(1, scaleDirection); break; default: @@ -228,7 +232,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy bodySprite.FillMode = FillMode.Stretch; // i dunno this looks about right?? - bodySprite.Scale = new Vector2(1, 32800 / sprite.DrawHeight); + bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight); } break; diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingSourceAttributeTest.cs similarity index 98% rename from osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs rename to osu.Game.Tests/Mods/SettingSourceAttributeTest.cs index bf59862787..5da303d3a7 100644 --- a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs +++ b/osu.Game.Tests/Mods/SettingSourceAttributeTest.cs @@ -12,7 +12,7 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Tests.Mods { [TestFixture] - public partial class SettingsSourceAttributeTest + public partial class SettingSourceAttributeTest { [Test] public void TestOrdering() diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk new file mode 100644 index 0000000000..c85f3b6352 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk differ diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 04b8c6dd4e..bd8088cfb6 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -48,7 +48,9 @@ namespace osu.Game.Tests.Skins // Covers BPM counter. "Archives/modified-default-20221205.osk", // Covers judgement counter. - "Archives/modified-default-20230117.osk" + "Archives/modified-default-20230117.osk", + // Covers player avatar and flag. + "Archives/modified-argon-20230305.osk", }; /// diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 467b943ea0..bedb2ceaa1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -15,6 +15,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Tests.Beatmaps.IO; @@ -188,6 +189,33 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); } + [Test] + public void TestChangeToNonSkinnableScreen() + { + advanceToSongSelect(); + openSkinEditor(); + AddAssert("blueprint container present", () => skinEditor.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddAssert("placeholder not present", () => skinEditor.ChildrenOfType().Count(), () => Is.Zero); + AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0)); + + AddStep("add skinnable component", () => + { + skinEditor.ChildrenOfType().First().TriggerClick(); + }); + AddUntilStep("newly added component selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(1)); + + AddStep("exit to main menu", () => Game.ScreenStack.CurrentScreen.Exit()); + AddAssert("selection cleared", () => skinEditor.SelectedComponents, () => Has.Count.Zero); + AddAssert("blueprint container not present", () => skinEditor.ChildrenOfType().Count(), () => Is.Zero); + AddAssert("placeholder present", () => skinEditor.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddAssert("editor sidebars empty", () => skinEditor.ChildrenOfType().SelectMany(sidebar => sidebar.Children).Count(), () => Is.Zero); + + advanceToSongSelect(); + AddAssert("blueprint container present", () => skinEditor.ChildrenOfType().Count(), () => Is.EqualTo(1)); + AddAssert("placeholder not present", () => skinEditor.ChildrenOfType().Count(), () => Is.Zero); + AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0)); + } + private void advanceToSongSelect() { PushAndConfirm(() => songSelect = new TestPlaySongSelect()); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 65ce0429fc..61a8322ee3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -516,15 +516,20 @@ namespace osu.Game.Tests.Visual.SongSelect { sets.Clear(); - for (int i = 0; i < 20; i++) + for (int i = 0; i < 10; i++) { var set = TestResources.CreateTestBeatmapSetInfo(5); - if (i >= 2 && i < 10) + // A total of 6 sets have date submitted (4 don't) + // A total of 5 sets have artist string (3 of which also have date submitted) + + if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i); - if (i < 5) + if (i < 5) // i = 0, 1, 2, 3, 4 have matching string set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string); + set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}"); + sets.Add(set); } }); @@ -532,15 +537,26 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(sets); AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false)); - checkVisibleItemCount(diff: false, count: 8); + checkVisibleItemCount(diff: false, count: 10); checkVisibleItemCount(diff: true, count: 5); + + AddAssert("missing date are at end", + () => carousel.Items.OfType().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(4)); + AddAssert("rest are at start", () => carousel.Items.OfType().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(), + () => Is.EqualTo(6)); + AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted, SearchText = zzz_string }, false)); - checkVisibleItemCount(diff: false, count: 3); + checkVisibleItemCount(diff: false, count: 5); checkVisibleItemCount(diff: true, count: 5); + + AddAssert("missing date are at end", + () => carousel.Items.OfType().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(2)); + AddAssert("rest are at start", () => carousel.Items.OfType().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(), + () => Is.EqualTo(3)); } [Test] @@ -1129,7 +1145,7 @@ namespace osu.Game.Tests.Visual.SongSelect { // until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet. AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () => - carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count); + carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible), () => Is.EqualTo(count)); } private void checkSelectionIsCentered() @@ -1190,8 +1206,11 @@ namespace osu.Game.Tests.Visual.SongSelect { get { - foreach (var item in Scroll.Children) + foreach (var item in Scroll.Children.OrderBy(c => c.Y)) { + if (item.Item?.Visible != true) + continue; + yield return item; if (item is DrawableCarouselBeatmapSet set) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 926bc01aea..77e7178c9e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -61,6 +61,18 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("scroll to 500", () => scroll.ScrollTo(500)); AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(scroll.Button); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); + + AddStep("user scroll down by 1", () => InputManager.ScrollVerticalBy(-1)); + + AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); } [Test] @@ -71,6 +83,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("invoke action", () => scroll.Button.Action.Invoke()); AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); + + AddStep("invoke action", () => scroll.Button.Action.Invoke()); + + AddAssert("scrolled to end", () => scroll.IsScrolledToEnd()); } [Test] @@ -85,6 +101,14 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(scroll.Button); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("scrolled to end", () => scroll.IsScrolledToEnd()); } [Test] @@ -97,12 +121,12 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button)); AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3); - AddAssert("invocation count is 1", () => invocationCount == 1); + AddAssert("invocation count is 3", () => invocationCount == 3); } private partial class TestScrollContainer : OverlayScrollContainer { - public new ScrollToTopButton Button => base.Button; + public new ScrollBackButton Button => base.Button; } } } diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index eab66b9857..3aab9a24e1 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; +using System.Collections.Generic; using osu.Framework.Localisation; namespace osu.Game.Beatmaps @@ -29,10 +29,21 @@ namespace osu.Game.Beatmaps return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim()); } - public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[] + public static List GetSearchableTerms(this IBeatmapInfo beatmapInfo) { - beatmapInfo.DifficultyName - }.Concat(beatmapInfo.Metadata.GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + var termsList = new List(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1); + + addIfNotNull(beatmapInfo.DifficultyName); + + BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList); + return termsList; + + void addIfNotNull(string? s) + { + if (!string.IsNullOrEmpty(s)) + termsList.Add(s); + } + } private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; } diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index 738bdb0839..fe4e815e62 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -3,7 +3,7 @@ #nullable disable -using System.Linq; +using System.Collections.Generic; using osu.Framework.Localisation; namespace osu.Game.Beatmaps @@ -13,16 +13,31 @@ namespace osu.Game.Beatmaps /// /// An array of all searchable terms provided in contained metadata. /// - public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) => new[] + public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) { - metadataInfo.Author.Username, - metadataInfo.Artist, - metadataInfo.ArtistUnicode, - metadataInfo.Title, - metadataInfo.TitleUnicode, - metadataInfo.Source, - metadataInfo.Tags - }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + var termsList = new List(MAX_SEARCHABLE_TERM_COUNT); + CollectSearchableTerms(metadataInfo, termsList); + return termsList.ToArray(); + } + + internal const int MAX_SEARCHABLE_TERM_COUNT = 7; + + internal static void CollectSearchableTerms(IBeatmapMetadataInfo metadataInfo, IList termsList) + { + addIfNotNull(metadataInfo.Author.Username); + addIfNotNull(metadataInfo.Artist); + addIfNotNull(metadataInfo.ArtistUnicode); + addIfNotNull(metadataInfo.Title); + addIfNotNull(metadataInfo.TitleUnicode); + addIfNotNull(metadataInfo.Source); + addIfNotNull(metadataInfo.Tags); + + void addIfNotNull(string s) + { + if (!string.IsNullOrEmpty(s)) + termsList.Add(s); + } + } /// /// A user-presentable display title representing this metadata. diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index 4b23f661f9..9edc213077 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -8,12 +8,12 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Collections { - public partial class DeleteCollectionDialog : DeleteConfirmationDialog + public partial class DeleteCollectionDialog : DangerousActionDialog { public DeleteCollectionDialog(Live collection, Action deleteAction) { BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})"); - DeleteAction = deleteAction; + DangerousAction = deleteAction; } } } diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index 3bc8bb6df1..e7af6cf70d 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -11,7 +11,6 @@ using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; -using osu.Framework.Graphics.Colour; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -259,8 +258,6 @@ namespace osu.Game.Graphics.Backgrounds Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix) ); - ColourInfo colourInfo = triangleColourInfo(DrawColourInfo.Colour, triangleQuad); - RectangleF textureCoords = new RectangleF( triangleQuad.TopLeft.X - topLeft.X, triangleQuad.TopLeft.Y - topLeft.Y, @@ -268,23 +265,12 @@ namespace osu.Game.Graphics.Backgrounds triangleQuad.Height ) / relativeSize; - renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); + renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); } shader.Unbind(); } - private static ColourInfo triangleColourInfo(ColourInfo source, Quad quad) - { - return new ColourInfo - { - TopLeft = source.Interpolate(quad.TopLeft), - TopRight = source.Interpolate(quad.TopRight), - BottomLeft = source.Interpolate(quad.BottomLeft), - BottomRight = source.Interpolate(quad.BottomRight) - }; - } - private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) { float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); diff --git a/osu.Game/Graphics/UserInterface/SegmentedGraph.cs b/osu.Game/Graphics/UserInterface/SegmentedGraph.cs index e8817d5275..91971e5af9 100644 --- a/osu.Game/Graphics/UserInterface/SegmentedGraph.cs +++ b/osu.Game/Graphics/UserInterface/SegmentedGraph.cs @@ -249,13 +249,7 @@ namespace osu.Game.Graphics.UserInterface private ColourInfo getSegmentColour(SegmentInfo segment) { - var segmentColour = new ColourInfo - { - TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)), - TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)), - BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)), - BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f)) - }; + var segmentColour = DrawColourInfo.Colour.Interpolate(new Quad(segment.Start, 0f, segment.End - segment.Start, 1f)); var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0); segmentColour.ApplyChild(tierColour); diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 547df86fc7..7c11ea6ac6 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -39,6 +39,16 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed."); + /// + /// "Corner radius" + /// + public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), "Corner radius"); + + /// + /// "How rounded the corners should be." + /// + public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), "How rounded the corners should be."); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/SkinEditorStrings.cs b/osu.Game/Localisation/SkinEditorStrings.cs index 5cf2e5b5c5..3c1d1ff40d 100644 --- a/osu.Game/Localisation/SkinEditorStrings.cs +++ b/osu.Game/Localisation/SkinEditorStrings.cs @@ -42,7 +42,12 @@ namespace osu.Game.Localisation /// /// "Currently editing" /// - public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), "Currently editing"); + public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), @"Currently editing"); + + /// + /// "All layout elements for layers in the current screen will be reset to defaults." + /// + public static LocalisableString RevertToDefaultDescription => new TranslatableString(getKey(@"revert_to_default_description"), @"All layout elements for layers in the current screen will be reset to defaults."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 682c96a695..fd5e0e9836 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat private const float chatting_text_width = 220; private const float search_icon_width = 40; + private const float padding = 5; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -71,9 +72,10 @@ namespace osu.Game.Overlays.Chat RelativeSizeAxes = Axes.Y, Width = chatting_text_width, Masking = true, - Padding = new MarginPadding { Right = 5 }, + Padding = new MarginPadding { Horizontal = padding }, Child = chattingText = new OsuSpriteText { + MaxWidth = chatting_text_width - padding * 2, Font = OsuFont.Torus.With(size: 20), Colour = colourProvider.Background1, Anchor = Anchor.CentreRight, @@ -97,7 +99,7 @@ namespace osu.Game.Overlays.Chat new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, + Padding = new MarginPadding { Right = padding }, Child = chatTextBox = new ChatTextBox { Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs similarity index 66% rename from osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs rename to osu.Game/Overlays/Dialog/DangerousActionDialog.cs index ddb59c4c9e..c86570386f 100644 --- a/osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -8,18 +8,22 @@ using osu.Game.Localisation; namespace osu.Game.Overlays.Dialog { /// - /// Base class for various confirmation dialogs that concern deletion actions. + /// A dialog which provides confirmation for actions which result in permanent consequences. /// Differs from in that the confirmation button is a "dangerous" one /// (requires the confirm button to be held). /// - public abstract partial class DeleteConfirmationDialog : PopupDialog + /// + /// The default implementation comes with text for a generic deletion operation. + /// This can be further customised by specifying custom . + /// + public abstract partial class DangerousActionDialog : PopupDialog { /// /// The action which performs the deletion. /// - protected Action? DeleteAction { get; set; } + protected Action? DangerousAction { get; set; } - protected DeleteConfirmationDialog() + protected DangerousActionDialog() { HeaderText = DeleteConfirmationDialogStrings.HeaderText; @@ -30,7 +34,7 @@ namespace osu.Game.Overlays.Dialog new PopupDialogDangerousButton { Text = DeleteConfirmationDialogStrings.Confirm, - Action = () => DeleteAction?.Invoke() + Action = () => DangerousAction?.Invoke() }, new PopupDialogCancelButton { diff --git a/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs b/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs index 800ebe8b4e..9788764453 100644 --- a/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs +++ b/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs @@ -7,12 +7,12 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public partial class DeleteModPresetDialog : DeleteConfirmationDialog + public partial class DeleteModPresetDialog : DangerousActionDialog { public DeleteModPresetDialog(Live modPreset) { BodyText = modPreset.PerformRead(preset => preset.Name); - DeleteAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true); + DangerousAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true); } } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 5bd7f014a9..9752e04f44 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,25 +22,29 @@ using osuTK.Graphics; namespace osu.Game.Overlays { /// - /// which provides . Mostly used in . + /// which provides . Mostly used in . /// public partial class OverlayScrollContainer : UserTrackingScrollContainer { /// - /// Scroll position at which the will be shown. + /// Scroll position at which the will be shown. /// private const int button_scroll_position = 200; - protected readonly ScrollToTopButton Button; + protected ScrollBackButton Button; - public OverlayScrollContainer() + private readonly Bindable lastScrollTarget = new Bindable(); + + [BackgroundDependencyLoader] + private void load() { - AddInternal(Button = new ScrollToTopButton + AddInternal(Button = new ScrollBackButton { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Margin = new MarginPadding(20), - Action = scrollToTop + Action = scrollBack, + LastScrollTarget = { BindTarget = lastScrollTarget } }); } @@ -53,16 +58,31 @@ namespace osu.Game.Overlays return; } - Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden; } - private void scrollToTop() + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { - ScrollToStart(); - Button.State = Visibility.Hidden; + base.OnUserScroll(value, animated, distanceDecay); + + lastScrollTarget.Value = null; } - public partial class ScrollToTopButton : OsuHoverContainer + private void scrollBack() + { + if (lastScrollTarget.Value == null) + { + lastScrollTarget.Value = Target; + ScrollToStart(); + } + else + { + ScrollTo(lastScrollTarget.Value.Value); + lastScrollTarget.Value = null; + } + } + + public partial class ScrollBackButton : OsuHoverContainer { private const int fade_duration = 500; @@ -88,8 +108,11 @@ namespace osu.Game.Overlays private readonly Container content; private readonly Box background; + private readonly SpriteIcon spriteIcon; - public ScrollToTopButton() + public Bindable LastScrollTarget = new Bindable(); + + public ScrollBackButton() : base(HoverSampleSet.ScrollToTop) { Size = new Vector2(50); @@ -113,7 +136,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, - new SpriteIcon + spriteIcon = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -134,6 +157,17 @@ namespace osu.Game.Overlays flashColour = colourProvider.Light1; } + protected override void LoadComplete() + { + base.LoadComplete(); + + LastScrollTarget.BindValueChanged(target => + { + spriteIcon.RotateTo(target.NewValue != null ? 180 : 0, fade_duration, Easing.OutQuint); + TooltipText = target.NewValue != null ? CommonStrings.ButtonsBackToPrevious : CommonStrings.ButtonsBackToTop; + }, true); + } + protected override bool OnClick(ClickEvent e) { background.FlashColour(flashColour, 800, Easing.OutQuint); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs index 948d646e3d..99ef62d94b 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs @@ -6,12 +6,12 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Overlays.Settings.Sections.Maintenance { - public partial class MassDeleteConfirmationDialog : DeleteConfirmationDialog + public partial class MassDeleteConfirmationDialog : DangerousActionDialog { public MassDeleteConfirmationDialog(Action deleteAction) { BodyText = "Everything?"; - DeleteAction = deleteAction; + DangerousAction = deleteAction; } } } diff --git a/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs b/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs new file mode 100644 index 0000000000..fa59d18de1 --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings +{ + /// + /// A that displays its value as a percentage by default. + /// Mostly provided for convenience of use with . + /// + public partial class SettingsPercentageSlider : SettingsSlider + where TValue : struct, IEquatable, IComparable, IConvertible + { + protected override Drawable CreateControl() => ((RoundedSliderBar)base.CreateControl()).With(sliderBar => sliderBar.DisplayAsPercentage = true); + } +} diff --git a/osu.Game/Overlays/SkinEditor/NonSkinnableScreenPlaceholder.cs b/osu.Game/Overlays/SkinEditor/NonSkinnableScreenPlaceholder.cs new file mode 100644 index 0000000000..daaa92035e --- /dev/null +++ b/osu.Game/Overlays/SkinEditor/NonSkinnableScreenPlaceholder.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Overlays.SkinEditor +{ + public partial class NonSkinnableScreenPlaceholder : CompositeDrawable + { + [Resolved] + private SkinEditorOverlay? skinEditorOverlay { get; set; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colourProvider.Dark6, + RelativeSizeAxes = Axes.Both, + Alpha = 0.95f, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Icon = FontAwesome.Solid.ExclamationCircle, + Size = new Vector2(24), + Y = -5, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 18)) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextAnchor = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = "Please navigate to a skinnable screen using the scene library", + }, + new RoundedButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 200, + Margin = new MarginPadding { Top = 20 }, + Action = () => skinEditorOverlay?.Hide(), + Text = "Return to game" + } + } + }, + }; + } + } +} diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index 08b4c85e4a..3f8d9f80d4 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -7,15 +7,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Framework.Testing; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Skinning; @@ -26,16 +18,16 @@ namespace osu.Game.Overlays.SkinEditor { public partial class SkinBlueprintContainer : BlueprintContainer { - private readonly Drawable target; + private readonly ISerialisableDrawableContainer targetContainer; private readonly List> targetComponents = new List>(); [Resolved] private SkinEditor editor { get; set; } = null!; - public SkinBlueprintContainer(Drawable target) + public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer) { - this.target = target; + this.targetContainer = targetContainer; } protected override void LoadComplete() @@ -44,22 +36,10 @@ namespace osu.Game.Overlays.SkinEditor SelectedItems.BindTo(editor.SelectedComponents); - // track each target container on the current screen. - var targetContainers = target.ChildrenOfType().ToArray(); + var bindableList = new BindableList { BindTarget = targetContainer.Components }; + bindableList.BindCollectionChanged(componentsChanged, true); - if (targetContainers.Length == 0) - { - AddInternal(new NonSkinnableScreenPlaceholder()); - return; - } - - foreach (var targetContainer in targetContainers) - { - var bindableList = new BindableList { BindTarget = targetContainer.Components }; - bindableList.BindCollectionChanged(componentsChanged, true); - - targetComponents.Add(bindableList); - } + targetComponents.Add(bindableList); } private void componentsChanged(object? sender, NotifyCollectionChangedEventArgs e) => Schedule(() => @@ -160,65 +140,5 @@ namespace osu.Game.Overlays.SkinEditor foreach (var list in targetComponents) list.UnbindAll(); } - - public partial class NonSkinnableScreenPlaceholder : CompositeDrawable - { - [Resolved] - private SkinEditorOverlay? skinEditorOverlay { get; set; } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = colourProvider.Dark6, - RelativeSizeAxes = Axes.Both, - Alpha = 0.95f, - }, - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0, 5), - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Icon = FontAwesome.Solid.ExclamationCircle, - Size = new Vector2(24), - Y = -5, - }, - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 18)) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - TextAnchor = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = "Please navigate to a skinnable screen using the scene library", - }, - new RoundedButton - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Width = 200, - Margin = new MarginPadding { Top = 20 }, - Action = () => skinEditorOverlay?.Hide(), - Text = "Return to game" - } - } - }, - }; - } - } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 02ede8c08b..2b23ce290f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -17,6 +17,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using Web = osu.Game.Resources.Localisation.Web; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Graphics; @@ -24,6 +25,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Overlays.Dialog; using osu.Game.Overlays.OSD; using osu.Game.Overlays.Settings; using osu.Game.Screens.Edit; @@ -97,6 +99,9 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private OnScreenDisplay? onScreenDisplay { get; set; } + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + public SkinEditor() { } @@ -147,8 +152,8 @@ namespace osu.Game.Overlays.SkinEditor { Items = new[] { - new EditorMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, revert), + new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), new EditorMenuItemSpacer(), new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), }, @@ -304,7 +309,8 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.Dispose(); // Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target. - content?.Clear(); + if (content?.Child is SkinBlueprintContainer) + content.Clear(); Scheduler.AddOnce(loadBlueprintContainer); Scheduler.AddOnce(populateSettings); @@ -323,17 +329,18 @@ namespace osu.Game.Overlays.SkinEditor foreach (var toolbox in componentsSidebar.OfType()) toolbox.Expire(); - if (target.NewValue == null) - return; + componentsSidebar.Clear(); + SelectedComponents.Clear(); Debug.Assert(content != null); - SelectedComponents.Clear(); - var skinComponentsContainer = getTarget(target.NewValue); - if (skinComponentsContainer == null) + if (target.NewValue == null || skinComponentsContainer == null) + { + content.Child = new NonSkinnableScreenPlaceholder(); return; + } changeHandler = new SkinEditorChangeHandler(skinComponentsContainer); changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); @@ -668,6 +675,16 @@ namespace osu.Game.Overlays.SkinEditor } } + public partial class RevertConfirmDialog : DangerousActionDialog + { + public RevertConfirmDialog(Action revert) + { + HeaderText = CommonStrings.RevertToDefault; + BodyText = SkinEditorStrings.RevertToDefaultDescription; + DangerousAction = revert; + } + } + #region Delegation of IEditorChangeHandler public event Action? OnStateChange diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index dc7594f469..d4223a80c2 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -7,7 +7,6 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Judgements; @@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.Mods public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo)); - [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsSlider))] + [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider))] public BindableNumber MinimumAccuracy { get; } = new BindableDouble { MinValue = 0.60, @@ -69,12 +68,4 @@ namespace osu.Game.Rulesets.Mods return scoreProcessor.ComputeAccuracy(score); } } - - public partial class PercentSlider : RoundedSliderBar - { - public PercentSlider() - { - DisplayAsPercentage = true; - } - } } diff --git a/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs b/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs index 68a0ef4250..8556949528 100644 --- a/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs +++ b/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs @@ -7,12 +7,12 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Edit { - public partial class DeleteDifficultyConfirmationDialog : DeleteConfirmationDialog + public partial class DeleteDifficultyConfirmationDialog : DangerousActionDialog { public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction) { BodyText = $"\"{beatmapInfo.DifficultyName}\" difficulty"; - DeleteAction = deleteAction; + DangerousAction = deleteAction; } } } diff --git a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs new file mode 100644 index 0000000000..1d0331593a --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Overlays.Settings; +using osu.Game.Skinning; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class PlayerAvatar : CompositeDrawable, ISerialisableDrawable + { + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription), + SettingControlType = typeof(SettingsPercentageSlider))] + public new BindableFloat CornerRadius { get; set; } = new BindableFloat(0.25f) + { + MinValue = 0, + MaxValue = 0.5f, + Precision = 0.01f + }; + + private readonly UpdateableAvatar avatar; + + private const float default_size = 80f; + + public PlayerAvatar() + { + Size = new Vector2(default_size); + + InternalChild = avatar = new UpdateableAvatar(isInteractive: false) + { + RelativeSizeAxes = Axes.Both, + Masking = true + }; + } + + [BackgroundDependencyLoader] + private void load(GameplayState gameplayState) + { + avatar.User = gameplayState.Score.ScoreInfo.User; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + CornerRadius.BindValueChanged(e => avatar.CornerRadius = e.NewValue * default_size, true); + } + + public bool UsesFixedAnchor { get; set; } + } +} diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs new file mode 100644 index 0000000000..85799c03d3 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -0,0 +1,36 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Skinning; +using osu.Game.Users.Drawables; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable + { + private readonly UpdateableFlag flag; + + private const float default_size = 40f; + + public PlayerFlag() + { + Size = new Vector2(default_size, default_size / 1.4f); + InternalChild = flag = new UpdateableFlag + { + RelativeSizeAxes = Axes.Both, + }; + } + + [BackgroundDependencyLoader] + private void load(GameplayState gameplayState) + { + flag.CountryCode = gameplayState.Score.ScoreInfo.User.CountryCode; + } + + public bool UsesFixedAnchor { get; set; } + } +} diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index c0f97a05e2..8efad451df 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -10,7 +10,7 @@ using osu.Game.Scoring; namespace osu.Game.Screens.Select { - public partial class BeatmapClearScoresDialog : DeleteConfirmationDialog + public partial class BeatmapClearScoresDialog : DangerousActionDialog { [Resolved] private ScoreManager scoreManager { get; set; } = null!; @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Select public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion) { BodyText = $"All local scores on {beatmapInfo.GetDisplayTitle()}"; - DeleteAction = () => + DangerousAction = () => { Task.Run(() => scoreManager.Delete(beatmapInfo)) .ContinueWith(_ => onCompletion); diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs index 4ab23c3a3a..e98af8cca2 100644 --- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs +++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs @@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Select { - public partial class BeatmapDeleteDialog : DeleteConfirmationDialog + public partial class BeatmapDeleteDialog : DangerousActionDialog { private readonly BeatmapSetInfo beatmapSet; @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load(BeatmapManager beatmapManager) { - DeleteAction = () => beatmapManager.Delete(beatmapSet); + DangerousAction = () => beatmapManager.Delete(beatmapSet); } } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 837939716b..7e48bc5cdd 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -26,6 +26,11 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); + Filtered.Value = !checkMatch(criteria); + } + + private bool checkMatch(FilterCriteria criteria) + { bool match = criteria.Ruleset == null || BeatmapInfo.Ruleset.ShortName == criteria.Ruleset.ShortName || @@ -34,8 +39,7 @@ namespace osu.Game.Screens.Select.Carousel if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { // only check ruleset equality or convertability for selected beatmap - Filtered.Value = !match; - return; + return match; } match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating); @@ -49,18 +53,38 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status); + if (!match) return false; + match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username); match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating); - if (match && criteria.SearchTerms.Length > 0) + if (!match) return false; + + if (criteria.SearchTerms.Length > 0) { - string[] terms = BeatmapInfo.GetSearchableTerms(); + var terms = BeatmapInfo.GetSearchableTerms(); foreach (string criteriaTerm in criteria.SearchTerms) - match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); + { + bool any = false; + + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (string term in terms) + { + if (!term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)) continue; + + any = true; + break; + } + + if (any) continue; + + match = false; + break; + } // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. // this should be done after text matching so we can prioritise matching numbers in metadata. @@ -71,13 +95,14 @@ namespace osu.Game.Screens.Select.Carousel } } - if (match) - match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; + if (!match) return false; + + match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo); - Filtered.Value = !match; + return match; } public override int CompareTo(FilterCriteria criteria, CarouselItem other) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index c52b81f915..67822a27ee 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Select.Carousel if (!(other is CarouselBeatmapSet otherSet)) return base.CompareTo(criteria, other); - int comparison = 0; + int comparison; switch (criteria.Sort) { @@ -87,11 +87,7 @@ namespace osu.Game.Screens.Select.Carousel break; case SortMode.DateRanked: - // Beatmaps which have no ranked date should already be filtered away in this mode. - if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null) - break; - - comparison = otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value); + comparison = Nullable.Compare(otherSet.BeatmapSet.DateRanked, BeatmapSet.DateRanked); break; case SortMode.LastPlayed: @@ -111,11 +107,7 @@ namespace osu.Game.Screens.Select.Carousel break; case SortMode.DateSubmitted: - // Beatmaps which have no submitted date should already be filtered away in this mode. - if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null) - break; - - comparison = otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value); + comparison = Nullable.Compare(otherSet.BeatmapSet.DateSubmitted, BeatmapSet.DateSubmitted); break; } @@ -153,12 +145,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - bool filtered = Items.All(i => i.Filtered.Value); - - filtered |= criteria.Sort == SortMode.DateRanked && BeatmapSet.DateRanked == null; - filtered |= criteria.Sort == SortMode.DateSubmitted && BeatmapSet.DateSubmitted == null; - - Filtered.Value = filtered; + Filtered.Value = Items.All(i => i.Filtered.Value); } public override string ToString() => BeatmapSet.ToString(); diff --git a/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs b/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs index e1aa662942..6157e8f6a5 100644 --- a/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs +++ b/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs @@ -8,14 +8,14 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Select.Carousel { - public partial class UpdateLocalConfirmationDialog : DeleteConfirmationDialog + public partial class UpdateLocalConfirmationDialog : DangerousActionDialog { public UpdateLocalConfirmationDialog(Action onConfirm) { HeaderText = PopupDialogStrings.UpdateLocallyModifiedText; BodyText = PopupDialogStrings.UpdateLocallyModifiedDescription; Icon = FontAwesome.Solid.ExclamationTriangle; - DeleteAction = onConfirm; + DangerousAction = onConfirm; } } } diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index 6349e9e5eb..c4add31a4f 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -10,7 +10,7 @@ using osu.Game.Beatmaps; namespace osu.Game.Screens.Select { - public partial class LocalScoreDeleteDialog : DeleteConfirmationDialog + public partial class LocalScoreDeleteDialog : DangerousActionDialog { private readonly ScoreInfo score; @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; Icon = FontAwesome.Regular.TrashAlt; - DeleteAction = () => scoreManager.Delete(score); + DangerousAction = () => scoreManager.Delete(score); } } } diff --git a/osu.Game/Screens/Select/SkinDeleteDialog.cs b/osu.Game/Screens/Select/SkinDeleteDialog.cs index 9e11629890..6612ae837a 100644 --- a/osu.Game/Screens/Select/SkinDeleteDialog.cs +++ b/osu.Game/Screens/Select/SkinDeleteDialog.cs @@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Select { - public partial class SkinDeleteDialog : DeleteConfirmationDialog + public partial class SkinDeleteDialog : DangerousActionDialog { private readonly Skin skin; @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load(SkinManager manager) { - DeleteAction = () => + DangerousAction = () => { manager.Delete(skin.SkinInfo.Value); manager.CurrentSkinInfo.SetDefault(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index f4b0692619..eec2cd6a60 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -58,7 +58,11 @@ namespace osu.Game.Storyboards.Drawables using (drawableVideo.BeginAbsoluteSequence(Video.StartTime)) { Schedule(() => drawableVideo.PlaybackPosition = Time.Current - Video.StartTime); + drawableVideo.FadeIn(500); + + using (drawableVideo.BeginDelayedSequence(drawableVideo.Duration - 500)) + drawableVideo.FadeOut(500); } } } diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index a208f3c7c4..8f8d7052e5 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -30,14 +28,14 @@ namespace osu.Game.Users.Drawables /// Perform an action in addition to showing the country ranking. /// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX). /// - public Action Action; + public Action? Action; public UpdateableFlag(CountryCode countryCode = CountryCode.Unknown) { CountryCode = countryCode; } - protected override Drawable CreateDrawable(CountryCode countryCode) + protected override Drawable? CreateDrawable(CountryCode countryCode) { if (countryCode == CountryCode.Unknown && !ShowPlaceholderOnUnknown) return null; @@ -56,8 +54,8 @@ namespace osu.Game.Users.Drawables }; } - [Resolved(canBeNull: true)] - private RankingsOverlay rankingsOverlay { get; set; } + [Resolved] + private RankingsOverlay? rankingsOverlay { get; set; } protected override bool OnClick(ClickEvent e) {