From 2cbf71c592e3b533395fd1b9e4dffbccde39cf9d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Mar 2025 18:42:36 +0900 Subject: [PATCH 001/281] Add `MultiplayerPlaylistItem` copy constructor + tests --- .../OnlinePlay/MultiplayerPlaylistItemTest.cs | 66 +++++++++++++++++++ osu.Game.Tests/osu.Game.Tests.csproj | 1 + .../Online/Rooms/MultiplayerPlaylistItem.cs | 25 +++++++ 3 files changed, 92 insertions(+) create mode 100644 osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs diff --git a/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs b/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs new file mode 100644 index 0000000000..6885a579fa --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Bogus; +using MessagePack; +using NUnit.Framework; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; + +namespace osu.Game.Tests.OnlinePlay +{ + [TestFixture] + public class MultiplayerPlaylistItemTest + { + [Test] + public void TestCloneMultiplayerPlaylistItem() + { + var faker = new Faker() + .StrictMode(true) + .RuleFor(o => o.ID, f => f.Random.Long()) + .RuleFor(o => o.OwnerID, f => f.Random.Int()) + .RuleFor(o => o.BeatmapID, f => f.Random.Int()) + .RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash()) + .RuleFor(o => o.RulesetID, f => f.Random.Int()) + .RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) })) + .RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) })) + .RuleFor(o => o.Expired, f => f.Random.Bool()) + .RuleFor(o => o.PlaylistOrder, f => f.Random.UShort()) + .RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset()) + .RuleFor(o => o.StarRating, f => f.Random.Double()) + .RuleFor(o => o.Freestyle, f => f.Random.Bool()); + + for (int i = 0; i < 100; i++) + { + MultiplayerPlaylistItem item = faker.Generate(); + Assert.That(MessagePackSerializer.SerializeToJson(item.Clone()), Is.EqualTo(MessagePackSerializer.SerializeToJson(item))); + } + } + + [Test] + public void TestConstructFromAPIModel() + { + var faker = new Faker() + .StrictMode(true) + .RuleFor(o => o.ID, f => f.Random.Long()) + .RuleFor(o => o.OwnerID, f => f.Random.Int()) + .RuleFor(o => o.BeatmapID, f => f.Random.Int()) + .RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash()) + .RuleFor(o => o.RulesetID, f => f.Random.Int()) + .RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) })) + .RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) })) + .RuleFor(o => o.Expired, f => f.Random.Bool()) + .RuleFor(o => o.PlaylistOrder, f => f.Random.UShort()) + .RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset()) + .RuleFor(o => o.StarRating, f => f.Random.Double()) + .RuleFor(o => o.Freestyle, f => f.Random.Bool()); + + for (int i = 0; i < 100; i++) + { + MultiplayerPlaylistItem initialItem = faker.Generate(); + MultiplayerPlaylistItem copiedItem = new MultiplayerPlaylistItem(new PlaylistItem(initialItem)); + Assert.That(MessagePackSerializer.SerializeToJson(copiedItem), Is.EqualTo(MessagePackSerializer.SerializeToJson(initialItem))); + } + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index a1f43505f0..c86f05c257 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,6 +1,7 @@  + diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 3234e28166..d4417f2de4 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -62,11 +62,17 @@ namespace osu.Game.Online.Rooms [Key(11)] public bool Freestyle { get; set; } + /// + /// Creates a new . + /// [SerializationConstructor] public MultiplayerPlaylistItem() { } + /// + /// Creates a new from an API . + /// public MultiplayerPlaylistItem(PlaylistItem item) { ID = item.ID; @@ -82,5 +88,24 @@ namespace osu.Game.Online.Rooms StarRating = item.Beatmap.StarRating; Freestyle = item.Freestyle; } + + /// + /// Creates a copy of this . + /// + public MultiplayerPlaylistItem Clone() => new MultiplayerPlaylistItem + { + ID = ID, + OwnerID = OwnerID, + BeatmapID = BeatmapID, + BeatmapChecksum = BeatmapChecksum, + RulesetID = RulesetID, + RequiredMods = RequiredMods.ToArray(), + AllowedMods = AllowedMods.ToArray(), + Expired = Expired, + PlaylistOrder = PlaylistOrder, + PlayedAt = PlayedAt, + StarRating = StarRating, + Freestyle = Freestyle, + }; } } From 0608058f5d5e613462c6be187a2c38f6e91b2d24 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Mar 2025 18:42:43 +0900 Subject: [PATCH 002/281] Fix beatmap checksum being lost --- osu.Game/Online/Rooms/PlaylistItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 817b42f503..68c1ba62d2 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -97,7 +97,7 @@ namespace osu.Game.Online.Rooms } public PlaylistItem(MultiplayerPlaylistItem item) - : this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating }) + : this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating, Checksum = item.BeatmapChecksum }) { ID = item.ID; OwnerID = item.OwnerID; From 1fc684c4301a5ecca466e402ff7385885c231798 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 14 Mar 2025 19:25:58 +0900 Subject: [PATCH 003/281] Add package to Android project too --- osu.Game.Tests.Android/osu.Game.Tests.Android.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index b02425eadd..a8fc9536b9 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -28,6 +28,7 @@ + From 05c57d7a3f07911c28117385b3ee6a3e698fa04a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 18 Mar 2025 16:23:49 +0900 Subject: [PATCH 004/281] Add RNG seed --- osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs b/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs index 6885a579fa..4a80c71c3d 100644 --- a/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs +++ b/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using Bogus; using MessagePack; using NUnit.Framework; @@ -12,6 +13,12 @@ namespace osu.Game.Tests.OnlinePlay [TestFixture] public class MultiplayerPlaylistItemTest { + [SetUp] + public void Setup() + { + Randomizer.Seed = new Random(1337); + } + [Test] public void TestCloneMultiplayerPlaylistItem() { From 160dd686ea234657662035ba421988f5489d3504 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 18 Mar 2025 16:31:08 +0900 Subject: [PATCH 005/281] Add documentation regarding copying behaviour --- osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs | 6 ++++++ osu.Game/Online/Rooms/PlaylistItem.cs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index d4417f2de4..d0f806e561 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -73,6 +73,9 @@ namespace osu.Game.Online.Rooms /// /// Creates a new from an API . /// + /// + /// This will create unique instances of the and arrays but NOT unique instances of the contained s. + /// public MultiplayerPlaylistItem(PlaylistItem item) { ID = item.ID; @@ -92,6 +95,9 @@ namespace osu.Game.Online.Rooms /// /// Creates a copy of this . /// + /// + /// This will create unique instances of the and arrays but NOT unique instances of the contained s. + /// public MultiplayerPlaylistItem Clone() => new MultiplayerPlaylistItem { ID = ID, diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 68c1ba62d2..427f31fc64 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -96,6 +96,12 @@ namespace osu.Game.Online.Rooms Beatmap = beatmap; } + /// + /// Creates a new from a . + /// + /// + /// This will create unique instances of the and arrays but NOT unique instances of the contained s. + /// public PlaylistItem(MultiplayerPlaylistItem item) : this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating, Checksum = item.BeatmapChecksum }) { From 1d80d4d046ad58c09497ca9820ede4686286a84a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 18 Mar 2025 16:45:33 +0900 Subject: [PATCH 006/281] Use `MemberwiseClone()` for shallow copy --- .../Online/Rooms/MultiplayerPlaylistItem.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index d0f806e561..f58a67294e 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -98,20 +98,12 @@ namespace osu.Game.Online.Rooms /// /// This will create unique instances of the and arrays but NOT unique instances of the contained s. /// - public MultiplayerPlaylistItem Clone() => new MultiplayerPlaylistItem + public MultiplayerPlaylistItem Clone() { - ID = ID, - OwnerID = OwnerID, - BeatmapID = BeatmapID, - BeatmapChecksum = BeatmapChecksum, - RulesetID = RulesetID, - RequiredMods = RequiredMods.ToArray(), - AllowedMods = AllowedMods.ToArray(), - Expired = Expired, - PlaylistOrder = PlaylistOrder, - PlayedAt = PlayedAt, - StarRating = StarRating, - Freestyle = Freestyle, - }; + MultiplayerPlaylistItem clone = (MultiplayerPlaylistItem)MemberwiseClone(); + clone.RequiredMods = RequiredMods.ToArray(); + clone.AllowedMods = AllowedMods.ToArray(); + return clone; + } } } From 36e78119ae302681106b1d22fb0dbee2f5d3ec1a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 15:06:01 +0900 Subject: [PATCH 007/281] Rename DrawableRoom -> RoomPanel --- .../TestSceneDrawableLoungeRoom.cs | 16 ++++++------ .../Multiplayer/TestSceneMultiplayer.cs | 12 ++++----- .../TestSceneMultiplayerLoungeSubScreen.cs | 24 +++++++++--------- .../TestSceneMultiplayerMatchSubScreen.cs | 4 +-- .../Multiplayer/TestSceneRoomListing.cs | 2 +- ...eDrawableRoom.cs => TestSceneRoomPanel.cs} | 25 +++++++++---------- .../TestScenePlaylistsLoungeSubScreen.cs | 4 +-- .../Lounge/Components/RoomListing.cs | 10 ++++---- .../{DrawableRoom.cs => RoomPanel.cs} | 4 +-- ...awableLoungeRoom.cs => LoungeRoomPanel.cs} | 6 ++--- ...DrawableMatchRoom.cs => MatchRoomPanel.cs} | 4 +-- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Playlists/PlaylistsRoomSubScreen.cs | 2 +- 13 files changed, 57 insertions(+), 58 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneDrawableRoom.cs => TestSceneRoomPanel.cs} (89%) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{DrawableRoom.cs => RoomPanel.cs} (99%) rename osu.Game/Screens/OnlinePlay/Lounge/{DrawableLoungeRoom.cs => LoungeRoomPanel.cs} (97%) rename osu.Game/Screens/OnlinePlay/Match/{DrawableMatchRoom.cs => MatchRoomPanel.cs} (95%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs index 459a90d096..f99c49a2dc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - private DrawableLoungeRoom drawableRoom = null!; + private LoungeRoomPanel panel = null!; private SearchTextBox searchTextBox = null!; private readonly ManualResetEventSlim allowResponseCallback = new ManualResetEventSlim(); @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Width = 500, Depth = float.MaxValue }, - drawableRoom = new DrawableLoungeRoom(room) + panel = new LoungeRoomPanel(room) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -87,16 +87,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestFocusViaKeyboardCommit() { - DrawableLoungeRoom.PasswordEntryPopover? popover = null; + LoungeRoomPanel.PasswordEntryPopover? popover = null; AddAssert("search textbox has focus", () => checkFocus(searchTextBox)); AddStep("click room twice", () => { - InputManager.MoveMouseTo(drawableRoom); + InputManager.MoveMouseTo(panel); InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType().SingleOrDefault()) != null); + AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType().SingleOrDefault()) != null); AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType().Single())); @@ -122,16 +122,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestFocusViaMouseCommit() { - DrawableLoungeRoom.PasswordEntryPopover? popover = null; + LoungeRoomPanel.PasswordEntryPopover? popover = null; AddAssert("search textbox has focus", () => checkFocus(searchTextBox)); AddStep("click room twice", () => { - InputManager.MoveMouseTo(drawableRoom); + InputManager.MoveMouseTo(panel); InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left); }); - AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType().SingleOrDefault()) != null); + AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType().SingleOrDefault()) != null); AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType().Single())); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index ec0117a990..c0507c184d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -269,7 +269,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); - AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); + AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room and immediately exit select", () => @@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); - AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); + AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); @@ -349,13 +349,13 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); - AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); + AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("join room", () => InputManager.Key(Key.Enter)); - DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null; + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); @@ -802,7 +802,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("refresh rooms", () => this.ChildrenOfType().Single().UpdateFilter()); - AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); + AddUntilStep("wait for room", () => this.ChildrenOfType().Any()); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("disable polling", () => this.ChildrenOfType().Single().TimeBetweenPolls.Value = 0); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs index 56187f8778..f1915233e0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); + AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); AddAssert("textbox has focus", () => InputManager.FocusedDrawable is OsuPasswordTextBox); @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("textbox lost focus", () => InputManager.FocusedDrawable is SearchTextBox); AddStep("hit escape", () => InputManager.Key(Key.Escape)); - AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } @@ -65,9 +65,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); + AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any()); AddStep("exit screen", () => Stack.Exit()); - AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); + AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any()); AddAssert("room not joined", () => !MultiplayerClient.RoomJoined); } @@ -75,12 +75,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithIncorrectPasswordViaButton() { - DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; + LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null; createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); @@ -94,12 +94,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithIncorrectPasswordViaEnter() { - DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; + LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null; createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong"); AddStep("press enter", () => InputManager.Key(Key.Enter)); @@ -113,12 +113,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithCorrectPassword() { - DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; + LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null; createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick()); @@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestJoinRoomWithPasswordViaKeyboardOnly() { - DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null; + LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null; createRooms(GenerateRooms(1, withPassword: true)); AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("attempt join room", () => InputManager.Key(Key.Enter)); - AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); + AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null); AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "password"); AddStep("press enter", () => InputManager.Key(Key.Enter)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e5e4921a17..514d53f5ce 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -330,10 +330,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); - AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0)); + AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0)); AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID)); - AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0)); + AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0)); } private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 27c5758afa..58473f5fa2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -201,6 +201,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private bool checkRoomSelected(Room? room) => selectedRoom.Value == room; private Room? getRoomInFlow(int index) => - (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room; + (container.ChildrenOfType>().First().FlowingChildren.ElementAt(index) as RoomPanel)?.Room; } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs similarity index 89% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index 021c0abf1d..9c8f9e3574 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -18,13 +18,12 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public partial class TestSceneDrawableRoom : OsuTestScene + public partial class TestSceneRoomPanel : OsuTestScene { [Cached] protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); @@ -129,24 +128,24 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestEnableAndDisablePassword() { - DrawableRoom drawableRoom = null!; + RoomPanel panel = null!; Room room = null!; - AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room + AddStep("create room", () => Child = panel = createLoungeRoom(room = new Room { Name = "Room with password", Type = MatchType.HeadToHead, })); - AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any()); + AddUntilStep("wait for panel load", () => panel.ChildrenOfType().Any()); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); AddStep("set password", () => room.Password = "password"); - AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType().Single().Alpha)); + AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().Single().Alpha)); AddStep("unset password", () => room.Password = string.Empty); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); } [Test] @@ -160,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Spacing = new Vector2(5), Children = new[] { - new DrawableMatchRoom(new Room + new MatchRoomPanel(new Room { Name = "A host-only room", QueueMode = QueueMode.HostOnly, @@ -169,7 +168,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { SelectedItem = new Bindable() }, - new DrawableMatchRoom(new Room + new MatchRoomPanel(new Room { Name = "An all-players, team-versus room", QueueMode = QueueMode.AllPlayers, @@ -178,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { SelectedItem = new Bindable() }, - new DrawableMatchRoom(new Room + new MatchRoomPanel(new Room { Name = "A round-robin room", QueueMode = QueueMode.AllPlayersRoundRobin, @@ -191,7 +190,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - private DrawableRoom createLoungeRoom(Room room) + private RoomPanel createLoungeRoom(Room room) { room.Host ??= new APIUser { Username = "peppy", Id = 2 }; @@ -204,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }).ToArray(); } - return new DrawableLoungeRoom(room) + return new LoungeRoomPanel(room) { MatchingFilter = true, SelectedRoom = selectedRoom diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index ceb3a32402..f2eeb5363a 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -61,9 +61,9 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("last room is not masked", () => checkRoomVisible(roomListing.DrawableRooms[^1])); } - private bool checkRoomVisible(DrawableRoom room) => + private bool checkRoomVisible(RoomPanel panel) => loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad - .Contains(room.ScreenSpaceDrawQuad.Centre); + .Contains(panel.ScreenSpaceDrawQuad.Centre); private void createRooms(params Room[] rooms) { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 0276601656..14edd13ec5 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -40,10 +40,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly Bindable selectedRoom = new Bindable(); - public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); + public IReadOnlyList DrawableRooms => roomFlow.FlowingChildren.Cast().ToArray(); private readonly ScrollContainer scroll; - private readonly FillFlowContainer roomFlow; + private readonly FillFlowContainer roomFlow; private const float display_scale = 0.8f; @@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = roomFlow = new FillFlowContainer + Child = roomFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return true; } - static bool matchPermissions(DrawableLoungeRoom room, RoomPermissionsFilter accessType) + static bool matchPermissions(LoungeRoomPanel room, RoomPermissionsFilter accessType) { switch (accessType) { @@ -183,7 +183,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in rooms) { - var drawableRoom = new DrawableLoungeRoom(room) + var drawableRoom = new LoungeRoomPanel(room) { SelectedRoom = selectedRoom, Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs similarity index 99% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index 491d8071f1..b9fe45d227 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -35,7 +35,7 @@ using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public abstract partial class DrawableRoom : CompositeDrawable, IHasContextMenu + public abstract partial class RoomPanel : CompositeDrawable, IHasContextMenu { protected const float CORNER_RADIUS = 10; private const float height = 100; @@ -63,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private UpdateableBeatmapBackgroundSprite background = null!; private DelayedLoadWrapper wrapper = null!; - protected DrawableRoom(Room room) + protected RoomPanel(Room room) { Room = room; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeRoomPanel.cs similarity index 97% rename from osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungeRoomPanel.cs index d369722e5f..ba9cea9931 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeRoomPanel.cs @@ -36,9 +36,9 @@ using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Lounge { /// - /// A with lounge-specific interactions such as selection and hover sounds. + /// A with lounge-specific interactions such as selection and hover sounds. /// - public partial class DrawableLoungeRoom : DrawableRoom, IFilterable, IHasPopover, IKeyBindingHandler + public partial class LoungeRoomPanel : RoomPanel, IFilterable, IHasPopover, IKeyBindingHandler { private const float transition_duration = 60; private const float selection_border_width = 4; @@ -63,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private Sample? sampleJoin; private Drawable selectionBox = null!; - public DrawableLoungeRoom(Room room) + public LoungeRoomPanel(Room room) : base(room) { } diff --git a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs b/osu.Game/Screens/OnlinePlay/Match/MatchRoomPanel.cs similarity index 95% rename from osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs rename to osu.Game/Screens/OnlinePlay/Match/MatchRoomPanel.cs index b10e83a05c..861cccde7b 100644 --- a/osu.Game/Screens/OnlinePlay/Match/DrawableMatchRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Match/MatchRoomPanel.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Match { - public partial class DrawableMatchRoom : DrawableRoom + public partial class MatchRoomPanel : RoomPanel { public Action? OnEdit; @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private readonly bool allowEdit; - public DrawableMatchRoom(Room room, bool allowEdit = true) + public MatchRoomPanel(Room room, bool allowEdit = true) : base(room) { this.allowEdit = allowEdit; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index f924ff6980..6603cd5692 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = new DrawableMatchRoom(Room, allowEdit) + Child = new MatchRoomPanel(Room, allowEdit) { OnEdit = () => settingsOverlay.Show(), SelectedItem = SelectedItem diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index ae31e55da5..d69c6edff3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -186,7 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable[] { - new DrawableMatchRoom(room, false) + new MatchRoomPanel(room, false) { OnEdit = () => settingsOverlay.Show(), SelectedItem = SelectedItem From ec0f8142c26e9937380f43a2ab9ee456ba37f3da Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 15:22:13 +0900 Subject: [PATCH 008/281] Add playlists/multiplayer versions of `RoomPanel` --- .../TestSceneMultiplayerMatchSubScreen.cs | 4 +- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 22 ++--- .../OnlinePlay/Match/MatchRoomPanel.cs | 92 ------------------- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 9 +- .../Multiplayer/MultiplayerRoomPanel.cs | 78 ++++++++++++++++ .../Playlists/PlaylistsRoomPanel.cs | 36 ++++++++ .../Playlists/PlaylistsRoomSubScreen.cs | 3 +- 7 files changed, 126 insertions(+), 118 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Match/MatchRoomPanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomPanel.cs create mode 100644 osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPanel.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 514d53f5ce..2c0ca1a926 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -330,10 +330,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); - AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0)); + AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0)); AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID)); - AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0)); + AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0)); } private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index 9c8f9e3574..fee5e62958 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -18,6 +18,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Beatmaps; using osuTK; @@ -159,33 +160,24 @@ namespace osu.Game.Tests.Visual.Multiplayer Spacing = new Vector2(5), Children = new[] { - new MatchRoomPanel(new Room + new MultiplayerRoomPanel(new Room { Name = "A host-only room", QueueMode = QueueMode.HostOnly, Type = MatchType.HeadToHead, - }) - { - SelectedItem = new Bindable() - }, - new MatchRoomPanel(new Room + }), + new MultiplayerRoomPanel(new Room { Name = "An all-players, team-versus room", QueueMode = QueueMode.AllPlayers, Type = MatchType.TeamVersus - }) - { - SelectedItem = new Bindable() - }, - new MatchRoomPanel(new Room + }), + new MultiplayerRoomPanel(new Room { Name = "A round-robin room", QueueMode = QueueMode.AllPlayersRoundRobin, Type = MatchType.HeadToHead - }) - { - SelectedItem = new Bindable() - }, + }), } }); } diff --git a/osu.Game/Screens/OnlinePlay/Match/MatchRoomPanel.cs b/osu.Game/Screens/OnlinePlay/Match/MatchRoomPanel.cs deleted file mode 100644 index 861cccde7b..0000000000 --- a/osu.Game/Screens/OnlinePlay/Match/MatchRoomPanel.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.ComponentModel; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Online.API; -using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Lounge.Components; -using osu.Game.Screens.OnlinePlay.Match.Components; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Match -{ - public partial class MatchRoomPanel : RoomPanel - { - public Action? OnEdit; - - public new required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - - public Drawable? ChangeSettingsButton { get; private set; } - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); - private readonly bool allowEdit; - - public MatchRoomPanel(Room room, bool allowEdit = true) - : base(room) - { - this.allowEdit = allowEdit; - - base.SelectedItem.BindTo(SelectedItem); - } - - [BackgroundDependencyLoader] - private void load() - { - if (allowEdit) - { - ButtonsContainer.Add(ChangeSettingsButton = new PurpleRoundedButton - { - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(120, 0.7f), - Text = "Change settings", - Action = () => OnEdit?.Invoke() - }); - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Room.PropertyChanged += onRoomPropertyChanged; - updateRoomHost(); - } - - private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(Room.Host)) - updateRoomHost(); - } - - private void updateRoomHost() - { - if (ChangeSettingsButton != null) - ChangeSettingsButton.Alpha = Room.Host?.Equals(api.LocalUser.Value) == true ? 1 : 0; - } - - protected override UpdateableBeatmapBackgroundSprite CreateBackground() => base.CreateBackground().With(d => - { - d.BackgroundLoadDelay = 0; - }); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - Room.PropertyChanged -= onRoomPropertyChanged; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6603cd5692..c9248449d3 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -100,7 +100,6 @@ namespace osu.Game.Screens.OnlinePlay.Match protected IBindable BeatmapAvailability => beatmapAvailabilityTracker.Availability; public readonly Room Room; - private readonly bool allowEdit; internal ModSelectOverlay UserModsSelectOverlay { get; private set; } = null!; @@ -112,12 +111,9 @@ namespace osu.Game.Screens.OnlinePlay.Match /// Creates a new . /// /// The . - /// Whether to allow editing room settings post-creation. - protected RoomSubScreen(Room room, bool allowEdit = true) + protected RoomSubScreen(Room room) { Room = room; - this.allowEdit = allowEdit; - Padding = new MarginPadding { Top = Header.HEIGHT }; } @@ -172,10 +168,9 @@ namespace osu.Game.Screens.OnlinePlay.Match { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = new MatchRoomPanel(Room, allowEdit) + Child = new MultiplayerRoomPanel(Room) { OnEdit = () => settingsOverlay.Show(), - SelectedItem = SelectedItem } } }, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomPanel.cs new file mode 100644 index 0000000000..e52133b46b --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomPanel.cs @@ -0,0 +1,78 @@ +// 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.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + /// + /// A to be displayed in a multiplayer lobby. + /// + public partial class MultiplayerRoomPanel : RoomPanel + { + public Action? OnEdit { get; set; } + + public Drawable ChangeSettingsButton { get; private set; } = null!; + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + public MultiplayerRoomPanel(Room room) + : base(room) + { + } + + [BackgroundDependencyLoader] + private void load() + { + ButtonsContainer.Add(ChangeSettingsButton = new PurpleRoundedButton + { + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(120, 0.7f), + Text = "Change settings", + Action = () => OnEdit?.Invoke() + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.RoomUpdated += onRoomUpdated; + onRoomUpdated(); + } + + private void onRoomUpdated() => Scheduler.AddOnce(() => + { + if (client.Room == null || client.LocalUser == null) + return; + + ChangeSettingsButton.Alpha = client.IsHost ? 1 : 0; + SelectedItem.Value = new PlaylistItem(client.Room.CurrentPlaylistItem); + }); + + protected override UpdateableBeatmapBackgroundSprite CreateBackground() => base.CreateBackground().With(d => + { + d.BackgroundLoadDelay = 0; + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPanel.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPanel.cs new file mode 100644 index 0000000000..d6c0f4dcbc --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomPanel.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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Lounge.Components; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + /// + /// A to be displayed in a playlists lobby. + /// + public partial class PlaylistsRoomPanel : RoomPanel + { + public new required Bindable SelectedItem + { + get => selectedItem.Current; + set => selectedItem.Current = value; + } + + private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); + + public PlaylistsRoomPanel(Room room) + : base(room) + { + base.SelectedItem.BindTo(SelectedItem); + } + + protected override UpdateableBeatmapBackgroundSprite CreateBackground() => base.CreateBackground().With(d => + { + d.BackgroundLoadDelay = 0; + }); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index d69c6edff3..c7b4d686dd 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -186,9 +186,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { new Drawable[] { - new MatchRoomPanel(room, false) + new PlaylistsRoomPanel(room) { - OnEdit = () => settingsOverlay.Show(), SelectedItem = SelectedItem } }, From 54b079098b4fac267982c81a3c1de1ebb604baa4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 19 Mar 2025 15:50:39 +0900 Subject: [PATCH 009/281] Fix code quality issue --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 2c0ca1a926..e51ea12e83 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -330,10 +330,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); - AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0)); + AddUntilStep("button visible", () => this.ChildrenOfType().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0)); AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID })); AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID)); - AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0)); + AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0)); } private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen From ea757029f11e1c1bb7a3cd6ceb4a58f4031949eb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 20 Mar 2025 16:36:35 +0900 Subject: [PATCH 010/281] Add package to iOS tests project --- osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index da07373037..9f13b0587b 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -29,6 +29,7 @@ + From 0209129618e63f5ca6d22bc9dd283f7735fb3466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Mar 2025 12:25:57 +0100 Subject: [PATCH 011/281] Extract leaderboard fetch logic from song select beatmap leaderboard drawable RFC. Another attempt at this. - Supersedes https://github.com/ppy/osu/pull/31881 - Supersedes / closes https://github.com/ppy/osu/pull/31355 - Closes https://github.com/ppy/osu/issues/29861 This is a weird diff because I am feeling rather boxed in by all the constraints, namely that: - Leaderboard state should be global state - But the global state is essentially managed by song select and namely `BeatmapLeaderboard` itself. That's because trying to e.g. not have `BeatmapLeaderboard` pass the beatmap and the ruleset to the global leaderboard manager is worse, as it essentially introduces two parallel paths of execution that need to be somehow merged into one (as in I'd have to somehow sync `LeaderboardManager` responding to beatmap/ruleset changes with `BeatmapLeaderboard` which is inheritance hell) - Also local leaderboard fetching is data-push (as in the scores can change under the leaderboard manager), and online leaderboard fetching is data-pull (as in the scores do not change unless the leaderboard manager does something). Also online leaderboard fetching can fail. Which is why I need to still have the weird setup wherein there's a `FetchWithCriteriaAsync()` (because I need to be able to respond to online requests taking time, or failing), but also the `BeatmapLeaderboard` only uses the public `Scores` bindable to actually read the scores (because it needs to respond to new local scores arriving). - Another thing to think about here is what happens when a retrieval fails because e.g. the user requested friend leaderboards without having supporter. With how this diff is written, that special condition is handled to `BeatmapLeaderboard`, and `LeaderboardManager`'s state will remain as whatever it was before that scope change was requested, which may be considered good or it may not (I imagine it's better to show scores in gameplay than not in this case, but maybe I'm wrong?) --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 3 + .../Online/Leaderboards/LeaderboardManager.cs | 162 ++++++++++++++++++ osu.Game/OsuGameBase.cs | 5 + .../Select/Leaderboards/BeatmapLeaderboard.cs | 120 +++---------- 4 files changed, 192 insertions(+), 98 deletions(-) create mode 100644 osu.Game/Online/Leaderboards/LeaderboardManager.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 474d2ee6e3..ebeba23123 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect private RulesetStore rulesetStore = null!; private BeatmapManager beatmapManager = null!; private PlaySongSelect songSelect = null!; + private LeaderboardManager leaderboardManager = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -52,6 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); dependencies.CacheAs(songSelect = new PlaySongSelect()); Dependencies.Cache(Realm); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); return dependencies; } @@ -60,6 +62,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void load() { LoadComponent(songSelect); + LoadComponent(leaderboardManager); } public TestSceneBeatmapLeaderboard() diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs new file mode 100644 index 0000000000..9104c83c02 --- /dev/null +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -0,0 +1,162 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; +using Realms; + +namespace osu.Game.Online.Leaderboards +{ + public partial class LeaderboardManager : Component + { + public IBindable Scores => scores; + private readonly Bindable scores = new Bindable(); + + private LeaderboardCriteria? criteria; + + private IDisposable? localScoreSubscription; + private TaskCompletionSource? localFetchCompletionSource; + private TaskCompletionSource? lastFetchCompletionSource; + private GetScoresRequest? inFlightOnlineRequest; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + public LeaderboardManager() + { + scores.BindValueChanged(_ => + { + if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource && scores.Value != null) + { + localFetchCompletionSource.SetResult(scores.Value); + localFetchCompletionSource = null; + } + }); + } + + public Task FetchWithCriteriaAsync(LeaderboardCriteria newCriteria) + { + if (criteria?.Equals(newCriteria) == true && lastFetchCompletionSource?.Task.IsFaulted == false) + return lastFetchCompletionSource?.Task ?? Task.FromResult(Scores.Value); + + criteria = newCriteria; + localScoreSubscription?.Dispose(); + inFlightOnlineRequest?.Cancel(); + lastFetchCompletionSource?.TrySetCanceled(); + scores.Value = null; + + switch (newCriteria.Scope) + { + case BeatmapLeaderboardScope.Local: + { + lastFetchCompletionSource = localFetchCompletionSource = new TaskCompletionSource(); + localScoreSubscription = realm.RegisterForNotifications(r => + r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" + + $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" + + $" AND {nameof(ScoreInfo.DeletePending)} == false" + , newCriteria.Beatmap.ID, newCriteria.Ruleset.ShortName), localScoresChanged); + return localFetchCompletionSource.Task; + } + + default: + { + var onlineFetchCompletionSource = new TaskCompletionSource(); + lastFetchCompletionSource = onlineFetchCompletionSource; + + IReadOnlyList? requestMods = null; + + if (newCriteria.ExactMods != null) + { + if (!newCriteria.ExactMods.Any()) + // add nomod for the request + requestMods = new Mod[] { new ModNoMod() }; + else + requestMods = newCriteria.ExactMods; + } + + var newRequest = new GetScoresRequest(newCriteria.Beatmap, newCriteria.Ruleset, newCriteria.Scope, requestMods); + newRequest.Success += response => + { + if (inFlightOnlineRequest != null && !newRequest.Equals(inFlightOnlineRequest)) + return; + + var result = new LeaderboardScores + ( + response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(), + response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) + ); + inFlightOnlineRequest = null; + if (onlineFetchCompletionSource.TrySetResult(result)) + scores.Value = result; + }; + newRequest.Failure += ex => onlineFetchCompletionSource.TrySetException(ex); + api.Queue(inFlightOnlineRequest = newRequest); + return onlineFetchCompletionSource.Task; + } + } + } + + private void localScoresChanged(IRealmCollection sender, ChangeSet? changes) + { + Debug.Assert(criteria != null); + + // This subscription may fire from changes to linked beatmaps, which we don't care about. + // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. + if (changes?.HasCollectionChanges() == false) + return; + + var newScores = sender.AsEnumerable(); + + if (criteria.ExactMods != null) + { + if (!criteria.ExactMods.Any()) + { + // we need to filter out all scores that have any mods to get all local nomod scores + newScores = newScores.Where(s => !s.Mods.Any()); + } + else + { + // otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters) + // we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself + var selectedMods = criteria.ExactMods.Select(m => m.Acronym).ToHashSet(); + + newScores = newScores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); + } + } + + newScores = newScores.Detach().OrderByTotalScore(); + + scores.Value = new LeaderboardScores(newScores, null); + } + } + + public record LeaderboardCriteria( + BeatmapInfo Beatmap, + RulesetInfo Ruleset, + BeatmapLeaderboardScope Scope, + Mod[]? ExactMods + ); + + public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore); +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4087a8b71e..fb28b8c5a4 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -49,6 +49,7 @@ using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Chat; +using osu.Game.Online.Leaderboards; using osu.Game.Online.Metadata; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; @@ -203,6 +204,7 @@ namespace osu.Game private UserLookupCache userCache; private BeatmapLookupCache beatmapCache; + private LeaderboardManager leaderboardManager; private RulesetConfigCache rulesetConfigCache; @@ -365,6 +367,9 @@ namespace osu.Game dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); + base.Content.Add(leaderboardManager); + // add api components to hierarchy. if (API is APIAccess apiAccess) base.Content.Add(apiAccess); diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 46705aaa28..e435554b03 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -3,21 +3,17 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using Realms; namespace osu.Game.Screens.Select.Leaderboards { @@ -67,6 +63,8 @@ namespace osu.Game.Screens.Select.Leaderboards } } + private readonly IBindable fetchedScores = new Bindable(); + [Resolved] private IBindable ruleset { get; set; } = null!; @@ -77,14 +75,7 @@ namespace osu.Game.Screens.Select.Leaderboards private IAPIProvider api { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } = null!; - - [Resolved] - private RealmAccess realm { get; set; } = null!; - - private IDisposable? scoreSubscription; - - private GetScoresRequest? scoreRetrievalRequest; + private LeaderboardManager leaderboardManager { get; set; } = null!; [BackgroundDependencyLoader] private void load() @@ -95,15 +86,23 @@ namespace osu.Game.Screens.Select.Leaderboards if (filterMods) RefetchScores(); }; + fetchedScores.BindTo(leaderboardManager.Scores); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + fetchedScores.BindValueChanged(_ => + { + if (fetchedScores.Value != null) + SetScores(fetchedScores.Value.TopScores, fetchedScores.Value.UserScore); + }); } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; protected override APIRequest? FetchScores(CancellationToken cancellationToken) { - scoreRetrievalRequest?.Cancel(); - scoreRetrievalRequest = null; - var fetchBeatmapInfo = BeatmapInfo; if (fetchBeatmapInfo == null) @@ -114,12 +113,6 @@ namespace osu.Game.Screens.Select.Leaderboards var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - if (Scope == BeatmapLeaderboardScope.Local) - { - subscribeToLocalScores(fetchBeatmapInfo, cancellationToken); - return null; - } - if (!api.IsLoggedIn) { SetErrorState(LeaderboardState.NotLoggedIn); @@ -132,7 +125,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) + if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && IsOnlineScope) { SetErrorState(LeaderboardState.BeatmapUnavailable); return null; @@ -150,29 +143,14 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - IReadOnlyList? requestMods = null; + leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)) + .ContinueWith(t => + { + if (t.Exception != null && !t.IsCanceled) + Schedule(() => SetErrorState(LeaderboardState.NetworkFailure)); + }, cancellationToken); - if (filterMods && !mods.Value.Any()) - // add nomod for the request - requestMods = new Mod[] { new ModNoMod() }; - else if (filterMods) - requestMods = mods.Value; - - var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); - newRequest.Success += response => Schedule(() => - { - // Request may have changed since fetch request. - // Can't rely on request cancellation due to Schedule inside SetScores so let's play it safe. - if (!newRequest.Equals(scoreRetrievalRequest)) - return; - - SetScores( - response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo)).OrderByTotalScore(), - response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo) - ); - }); - - return scoreRetrievalRequest = newRequest; + return null; } protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope, Scope != BeatmapLeaderboardScope.Friend) @@ -184,59 +162,5 @@ namespace osu.Game.Screens.Select.Leaderboards { Action = () => ScoreSelected?.Invoke(model) }; - - private void subscribeToLocalScores(BeatmapInfo beatmapInfo, CancellationToken cancellationToken) - { - Debug.Assert(beatmapInfo != null); - - scoreSubscription?.Dispose(); - scoreSubscription = null; - - scoreSubscription = realm.RegisterForNotifications(r => - r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0" - + $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" - + $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1" - + $" AND {nameof(ScoreInfo.DeletePending)} == false" - , beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged); - - void localScoresChanged(IRealmCollection sender, ChangeSet? changes) - { - if (cancellationToken.IsCancellationRequested) - return; - - // This subscription may fire from changes to linked beatmaps, which we don't care about. - // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. - if (changes?.HasCollectionChanges() == false) - return; - - var scores = sender.AsEnumerable(); - - if (filterMods && !mods.Value.Any()) - { - // we need to filter out all scores that have any mods to get all local nomod scores - scores = scores.Where(s => !s.Mods.Any()); - } - else if (filterMods) - { - // otherwise find all the scores that have all of the currently selected mods (similar to how web applies mod filters) - // we're creating and using a string HashSet representation of selected mods so that it can be translated into the DB query itself - var selectedMods = mods.Value.Select(m => m.Acronym).ToHashSet(); - - scores = scores.Where(s => selectedMods.SetEquals(s.Mods.Select(m => m.Acronym))); - } - - scores = scores.Detach().OrderByTotalScore(); - - SetScores(scores); - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - scoreSubscription?.Dispose(); - scoreRetrievalRequest?.Cancel(); - } } } From 748e890ee4a627e0ad4e56bbbc38698dfaf8e6a1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Mar 2025 13:56:40 +0900 Subject: [PATCH 012/281] Refactor multiplayer background to remove selected item bindable --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 5 +-- .../MultiplayerRoomBackgroundScreen.cs | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index f924ff6980..acf33ec59d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -39,10 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Match public override bool? ApplyModTrackAdjustments => true; - protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault()) - { - SelectedItem = { BindTarget = SelectedItem } - }; + protected override BackgroundScreen CreateBackground() => new MultiplayerRoomBackgroundScreen(); public override bool DisallowExternalBeatmapRulesetChanges => true; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs new file mode 100644 index 0000000000..a31b002095 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs @@ -0,0 +1,41 @@ +// 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.ObjectExtensions; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class MultiplayerRoomBackgroundScreen : OnlinePlayBackgroundScreen + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.RoomUpdated += onRoomUpdated; + onRoomUpdated(); + } + + private void onRoomUpdated() => Scheduler.AddOnce(() => + { + if (client.Room == null) + return; + + PlaylistItem = new PlaylistItem(client.Room.CurrentPlaylistItem); + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } + } +} From d9efa086730ac046a8b81e6583fee1a8d7a510c6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Mar 2025 16:11:51 +0900 Subject: [PATCH 013/281] Make class partial --- .../OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs index a31b002095..6cb3b7c688 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomBackgroundScreen.cs @@ -9,7 +9,7 @@ using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public class MultiplayerRoomBackgroundScreen : OnlinePlayBackgroundScreen + public partial class MultiplayerRoomBackgroundScreen : OnlinePlayBackgroundScreen { [Resolved] private MultiplayerClient client { get; set; } = null!; From 203e294e490c3821c574d6ef5c66f7c2ce6fc5c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Mar 2025 16:09:26 +0900 Subject: [PATCH 014/281] Fix storyboards with no-op alpha operations causing extended drawable lifetimes Test with https://osu.ppy.sh/beatmapsets/139525#osu/348550. See https://github.com/ppy/osu/issues/32453#issuecomment-2742562780 (and inline comments) for explanation. Closes https://github.com/ppy/osu/issues/32453. --- osu.Game/Storyboards/StoryboardSprite.cs | 37 ++++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 42426c8c85..968c2be929 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -28,31 +28,44 @@ namespace osu.Game.Storyboards { get { - // To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero. - // A StartValue of zero governs, above all else, the first valid display time of a sprite. + // Users that are crafting storyboards using raw osb scripting or external tools may create alpha events far before the actual display time + // of sprites. // - // You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero, - // anything before that point can be ignored (the sprite is not visible after all). - var alphaCommands = new List<(double startTime, bool isZeroStartValue)>(); + // To make sure lifetime optimisations work as efficiently as they can, let's locally find the first time a sprite becomes visible. + var alphaCommands = new List>(); - var command = Commands.Alpha.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); + foreach (var command in Commands.Alpha) + { + alphaCommands.Add(command); + if (visibleAtStartOrEnd(command)) + break; + } foreach (var loop in loopingGroups) { - command = loop.Alpha.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); + foreach (var command in loop.Alpha) + { + alphaCommands.Add(command); + if (visibleAtStartOrEnd(command)) + break; + } } if (alphaCommands.Count > 0) { - var firstAlpha = alphaCommands.MinBy(t => t.startTime); + // Special care is given to cases where there's one or more no-op transforms (ie transforming from alpha 0 to alpha 0). + // - If a 0->0 transform exists, we still need to check it to ensure the absolute first start value is non-visible. + // - After ascertaining this, we then check the first non-noop transform to get the true start lifetime. + var firstAlpha = alphaCommands.MinBy(c => c.StartTime); + var firstRealAlpha = alphaCommands.Where(visibleAtStartOrEnd).MinBy(c => c.StartTime); - if (firstAlpha.isZeroStartValue) - return firstAlpha.startTime; + if (firstAlpha!.StartValue == 0) + return firstRealAlpha!.StartTime; } return EarliestTransformTime; + + bool visibleAtStartOrEnd(StoryboardCommand command) => command.StartValue > 0 || command.EndValue > 0; } } From 6cd4a36c61b7a58ec1195048e4a4cbc2207d72f7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Mar 2025 16:17:54 +0900 Subject: [PATCH 015/281] Add multiplayer user mod display --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 4 +- .../Multiplayer/MultiplayerUserModDisplay.cs | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerUserModDisplay.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 2b3243e01d..6c6932f479 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -31,7 +31,6 @@ using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; -using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osuTK; using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList; @@ -180,11 +179,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Text = "Select", Action = ShowUserModSelect, }, - new ModDisplay + new MultiplayerUserModDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Current = UserMods, Scale = new Vector2(0.8f), }, } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerUserModDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerUserModDisplay.cs new file mode 100644 index 0000000000..8937feed5e --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerUserModDisplay.cs @@ -0,0 +1,67 @@ +// 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.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerUserModDisplay : CompositeDrawable + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private ModDisplay modDisplay = null!; + + public MultiplayerUserModDisplay() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = modDisplay = new ModDisplay(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.RoomUpdated += onRoomUpdated; + onRoomUpdated(); + } + + private void onRoomUpdated() => Scheduler.AddOnce(() => + { + if (client.Room == null || client.LocalUser == null) + return; + + MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem; + Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance(); + Mod[] userMods = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).ToArray(); + + if (!userMods.SequenceEqual(modDisplay.Current.Value)) + modDisplay.Current.Value = userMods; + }); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } + } +} From 3ff05b7330ef50dcf9041cd35f69229f6b340d0e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 25 Mar 2025 16:19:35 +0900 Subject: [PATCH 016/281] Add tests --- .../TestSceneMultiplayerUserModDisplay.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerUserModDisplay.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerUserModDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerUserModDisplay.cs new file mode 100644 index 0000000000..02b97c6dd6 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerUserModDisplay.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 System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.OnlinePlay.Multiplayer; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public partial class TestSceneMultiplayerUserModDisplay : MultiplayerTestScene + { + private MultiplayerUserModDisplay modDisplay = null!; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); + WaitForJoined(); + + AddStep("add display", () => Child = modDisplay = new MultiplayerUserModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + [Test] + public void TestChangeMods() + { + AddStep("set DT", () => MultiplayerClient.ChangeUserMods([new OsuModDoubleTime()]).WaitSafely()); + AddUntilStep("mod displayed", () => modDisplay.ChildrenOfType().Count() == 1); + + AddStep("set DT, HR", () => MultiplayerClient.ChangeUserMods([new OsuModDoubleTime(), new OsuModHardRock()]).WaitSafely()); + AddUntilStep("mods displayed", () => modDisplay.ChildrenOfType().Count() == 2); + + AddStep("set no mods", () => MultiplayerClient.ChangeUserMods(Enumerable.Empty()).WaitSafely()); + AddUntilStep("no mods displayed", () => !modDisplay.ChildrenOfType().Any()); + } + } +} From 199bcd7fdb17519bb9c6d298b0459c260864f016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Mar 2025 12:56:31 +0100 Subject: [PATCH 017/281] Improve input handling in beatmap card buttons This is in response to feedback in https://osu.ppy.sh/community/forums/topics/2056547?n=1. Upon examining the button further, there was indeed some rather weird... almost hysteresis in how the button behaved with respect to the area on the screen that activated it. Because of the following scourge of a method that continues to haunt us to this day: https://github.com/ppy/osu/blob/31487545d0d17c4337d4b4cc5d4afb3ba1dae838/osu.Game/Graphics/Containers/OsuClickableContainer.cs#L24-L25 the button would effectively only be activated by 80% of its drawable area when it was not hovered, because of the scale applied to the `content` container which `Container.Content` redirected to. This is resolved here by various rearrangements of paddings and sizes such that the clickable area of any of the buttons of the card is always the full top or bottom half of the button area. Also included are some cosmetic touch-ups which happened to be convenient like folding the loading spinner into the base `BeatmapCardIconButton`, adding loading support for the favourite button, using BDL more, and resolving some "virtual member call in constructor" inspections. --- .../Cards/Buttons/BeatmapCardIconButton.cs | 60 +++++++++---------- .../Drawables/Cards/Buttons/DownloadButton.cs | 15 +---- .../Cards/Buttons/FavouriteButton.cs | 7 ++- .../Cards/Buttons/GoToBeatmapButton.cs | 5 +- .../Cards/CollapsibleButtonContainer.cs | 9 +-- 5 files changed, 40 insertions(+), 56 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs index e78fd651fe..f9a1744f5c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -43,61 +44,48 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons } } - private float iconSize; + protected SpriteIcon Icon { get; private set; } = null!; - public float IconSize + private Container content = null!; + private Container hover = null!; + private LoadingSpinner spinner = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - get => iconSize; - set - { - iconSize = value; - Icon.Size = new Vector2(iconSize); - } - } + RelativeSizeAxes = Axes.Both; - protected readonly SpriteIcon Icon; - - protected override Container Content => content; - - private readonly Container content; - private readonly Box hover; - - protected BeatmapCardIconButton() - { - Origin = Anchor.Centre; - Anchor = Anchor.Centre; - - base.Content.Add(content = new Container + Add(content = new Container { RelativeSizeAxes = Axes.Both, Masking = true, - CornerRadius = BeatmapCard.CORNER_RADIUS, Scale = new Vector2(0.8f), Origin = Anchor.Centre, Anchor = Anchor.Centre, Children = new Drawable[] { - hover = new Box + hover = new Container { RelativeSizeAxes = Axes.Both, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Masking = true, Colour = Color4.White.Opacity(0.1f), Blending = BlendingParameters.Additive, + Child = new Box { RelativeSizeAxes = Axes.Both, } }, Icon = new SpriteIcon { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Scale = new Vector2(1.2f), + Size = new Vector2(14), + }, + spinner = new LoadingSpinner + { + Size = new Vector2(14), }, } }); - IconSize = 12; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { IdleColour = colourProvider.Light1; HoverColour = colourProvider.Content1; } @@ -127,8 +115,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons bool isHovered = IsHovered && Enabled.Value; hover.FadeTo(isHovered ? 1f : 0f, 500, Easing.OutQuint); - content.ScaleTo(isHovered ? 1 : 0.8f, 500, Easing.OutQuint); + content.ScaleTo(isHovered ? 0.9f : 0.8f, 500, Easing.OutQuint); Icon.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + spinner.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + } + + protected void SetLoading(bool isLoading) + { + Icon.Alpha = isLoading ? 0 : 1; + spinner.Alpha = isLoading ? 1 : 0; + Enabled.Value = !isLoading; } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs index 7f23b46150..96ec9d0731 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs @@ -8,10 +8,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Resources.Localisation.Web; -using osuTK; namespace osu.Game.Beatmaps.Drawables.Cards.Buttons { @@ -23,17 +21,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private Bindable preferNoVideo = null!; - private readonly LoadingSpinner spinner; - [Resolved] private BeatmapModelDownloader beatmaps { get; set; } = null!; public DownloadButton(APIBeatmapSet beatmapSet) { - Icon.Icon = FontAwesome.Solid.Download; - - Content.Add(spinner = new LoadingSpinner { Size = new Vector2(IconSize) }); - this.beatmapSet = beatmapSet; } @@ -41,6 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private void load(OsuConfigManager config) { preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); + Icon.Icon = FontAwesome.Solid.Download; } protected override void LoadComplete() @@ -64,8 +57,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons case DownloadState.Importing: Action = null; TooltipText = string.Empty; - spinner.Show(); - Icon.Hide(); + SetLoading(true); break; case DownloadState.LocallyAvailable: @@ -84,8 +76,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value); this.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - spinner.Hide(); - Icon.Show(); + SetLoading(false); if (!beatmapSet.HasVideo) TooltipText = BeatmapsetsStrings.PanelDownloadAll; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs index f698185863..0b2aaf0bc3 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs @@ -53,19 +53,20 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons favouriteRequest?.Cancel(); favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType); - Enabled.Value = false; + SetLoading(true); + favouriteRequest.Success += () => { bool favourited = actionType == BeatmapFavouriteAction.Favourite; current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1)); - Enabled.Value = true; + SetLoading(false); }; favouriteRequest.Failure += e => { Logger.Error(e, $"Failed to {actionType.ToString().ToLowerInvariant()} beatmap: {e.Message}"); - Enabled.Value = true; + SetLoading(false); }; api.Queue(favouriteRequest); diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs index 3df94bf233..e95ac94457 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/GoToBeatmapButton.cs @@ -20,15 +20,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons public GoToBeatmapButton(APIBeatmapSet beatmapSet) { this.beatmapSet = beatmapSet; - - Icon.Icon = FontAwesome.Solid.AngleDoubleRight; - TooltipText = "Go to beatmap"; } [BackgroundDependencyLoader(true)] private void load(OsuGame? game) { Action = () => game?.PresentBeatmap(beatmapSet); + Icon.Icon = FontAwesome.Solid.AngleDoubleRight; + TooltipText = "Go to beatmap"; } protected override void LoadComplete() diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs index a29724032e..5ab6e1a218 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs @@ -95,9 +95,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards Child = buttons = new Container { RelativeSizeAxes = Axes.Both, - // Padding of 4 avoids touching the card borders when in the expanded (ie. showing difficulties) state. - // Left override allows the buttons to visually be wider and look better. - Padding = new MarginPadding(4) { Left = 2 }, Children = new BeatmapCardIconButton[] { new FavouriteButton(beatmapSet) @@ -106,7 +103,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Both, - Height = 0.48f, + Height = 0.5f, }, new DownloadButton(beatmapSet) { @@ -114,7 +111,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Origin = Anchor.BottomCentre, State = { BindTarget = downloadTracker.State }, RelativeSizeAxes = Axes.Both, - Height = 0.48f, + Height = 0.5f, }, new GoToBeatmapButton(beatmapSet) { @@ -122,7 +119,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards Origin = Anchor.BottomCentre, State = { BindTarget = downloadTracker.State }, RelativeSizeAxes = Axes.Both, - Height = 0.48f, + Height = 0.5f, } } } From 491f28c451bea1f1f73be6a54210a74d92e87a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Mar 2025 20:59:25 +0100 Subject: [PATCH 018/281] Fix tests --- .../Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs index c33033624a..81abe105f1 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs @@ -91,6 +91,6 @@ namespace osu.Game.Tests.Visual.Beatmaps } private void assertCorrectIcon(bool favourited) => AddAssert("icon correct", - () => this.ChildrenOfType().Single().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart)); + () => this.ChildrenOfType().First().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart)); } } From 78de898f7fc7da86a4d449e7703b1441649e28a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Mar 2025 18:41:20 +0900 Subject: [PATCH 019/281] Avoid large sample depool overhead on drawable hitobjects with many nested objects Closes https://github.com/ppy/osu/issues/32588. --- .../TestSceneSpinner.cs | 7 ++- .../Editing/TestSceneEditorSamplePlayback.cs | 10 ++--- .../Objects/Drawables/DrawableHitObject.cs | 45 +++++++++++++++---- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 77b16dd0c5..2e082c292b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -86,9 +86,12 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestSpinningSamplePitchShift() { + PausableSkinnableSound spinSample = null; + AddStep("Add spinner", () => SetContents(_ => testSingle(5, true, 4000))); - AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8); - AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8); + AddUntilStep("wait for spin sample", () => (spinSample = getSpinningSample()) != null); + AddUntilStep("Pitch starts low", () => spinSample.Frequency.Value < 0.8); + AddUntilStep("Pitch increases", () => spinSample.Frequency.Value > 0.8); PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin")))); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs index 8b941d7597..092b2bc01c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs @@ -24,12 +24,7 @@ namespace osu.Game.Tests.Visual.Editing PoolableSkinnableSample[] loopingSamples = null; PoolableSkinnableSample[] onceOffSamples = null; - AddStep("get first slider", () => - { - slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); - onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); - loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); - }); + AddStep("get first slider", () => slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First()); AddStep("start playback", () => EditorClock.Start()); @@ -38,6 +33,9 @@ namespace osu.Game.Tests.Visual.Editing if (!slider.Tracking.Value) return false; + onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); + loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); + if (!loopingSamples.Any(s => s.Playing)) return false; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1f735576bc..db01da730f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; @@ -63,6 +62,8 @@ namespace osu.Game.Rulesets.Objects.Drawables protected PausableSkinnableSound Samples { get; private set; } + private bool samplesLoaded; + public virtual IEnumerable GetSamples() => HitObject.Samples; private readonly List nestedHitObjects = new List(); @@ -227,6 +228,12 @@ namespace osu.Game.Rulesets.Objects.Drawables comboColourBrightness.BindValueChanged(_ => UpdateComboColour()); + samplesBindable.BindCollectionChanged((_, _) => + { + if (samplesLoaded) + LoadSamples(); + }); + // Apply transforms updateStateFromResult(); } @@ -293,8 +300,6 @@ namespace osu.Game.Rulesets.Objects.Drawables } samplesBindable.BindTo(HitObject.SamplesBindable); - samplesBindable.BindCollectionChanged(onSamplesChanged, true); - HitObject.DefaultsApplied += onDefaultsApplied; OnApply(); @@ -335,11 +340,8 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.UnbindFrom(HitObject.SamplesBindable); - // When a new hitobject is applied, the samples will be cleared before re-populating. - // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). - samplesBindable.CollectionChanged -= onSamplesChanged; - // Release the samples for other hitobjects to use. + samplesLoaded = false; Samples?.ClearSamples(); foreach (var obj in nestedHitObjects) @@ -396,8 +398,6 @@ namespace osu.Game.Rulesets.Objects.Drawables Samples.Samples = samples.Cast().ToArray(); } - private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); - private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onRevertResult() @@ -631,6 +631,33 @@ namespace osu.Game.Rulesets.Objects.Drawables #endregion + protected override void Update() + { + // We use a flag here to load samples only when they are required to be played. + // Why in Update and not PlaySamples? Because some hit object implementations may expect LoadSamples to be called to load custom samples + // (slider slide sound as an example). + // + // This is best effort optimisation (over previous method of loading and de-pooling in `OnApply`) due to requiring knowledge of + // hitobjects' metadata. For cases like sliders with many repeats, there can be a sudden request to de-pool (ie slider with many repeats) + // hundreds of samples, causing a gameplay stutter. + // + // Note that we already have optimisations in OsuPlayfield for this but it applies to DrawableHitObjects and not samples. + // + // This is definitely not the end of optimisation of sample loading, but the structure of gameplay samples is going to take some + // time to dismantle and optimise. Optimally: + // + // - we would want to remove as much of the drawable overheads from samples as possible (currently two drawables per sample worst case) + // - pool the rawest representation of samples possible (if required at that point). + // - infer metadata at beatmap load to asynchronously preload the samples (into memory / bass). + if (!samplesLoaded) + { + samplesLoaded = true; + LoadSamples(); + } + + base.Update(); + } + public override bool UpdateSubTreeMasking() => false; protected override void UpdateAfterChildren() From 2b471919e923db34ac0b0deb5c29e1190bf36b2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Mar 2025 19:02:26 +0900 Subject: [PATCH 020/281] Remove loading spinner and fade out icon instead --- .../Drawables/Cards/Buttons/BeatmapCardIconButton.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs index f9a1744f5c..e4bcae281c 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -48,7 +47,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons private Container content = null!; private Container hover = null!; - private LoadingSpinner spinner = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -79,10 +77,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons Anchor = Anchor.Centre, Size = new Vector2(14), }, - spinner = new LoadingSpinner - { - Size = new Vector2(14), - }, } }); @@ -117,13 +111,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons hover.FadeTo(isHovered ? 1f : 0f, 500, Easing.OutQuint); content.ScaleTo(isHovered ? 0.9f : 0.8f, 500, Easing.OutQuint); Icon.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); - spinner.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); } protected void SetLoading(bool isLoading) { - Icon.Alpha = isLoading ? 0 : 1; - spinner.Alpha = isLoading ? 1 : 0; + Icon.FadeTo(isLoading ? 0.2f : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); Enabled.Value = !isLoading; } } From dd7026f7c71e422e775d73f06e0ec9de145ba7f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Mar 2025 19:40:19 +0900 Subject: [PATCH 021/281] Fix test failures due to `StoryboarVideo` having a weird initialisation flow --- osu.Game/Storyboards/StoryboardSprite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 968c2be929..49fa5d85c3 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -59,8 +59,8 @@ namespace osu.Game.Storyboards var firstAlpha = alphaCommands.MinBy(c => c.StartTime); var firstRealAlpha = alphaCommands.Where(visibleAtStartOrEnd).MinBy(c => c.StartTime); - if (firstAlpha!.StartValue == 0) - return firstRealAlpha!.StartTime; + if (firstAlpha!.StartValue == 0 && firstRealAlpha != null) + return firstRealAlpha.StartTime; } return EarliestTransformTime; From 4b62ea8d7402501b5a9e4041ab9279f1a44eaa83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Mar 2025 19:45:54 +0900 Subject: [PATCH 022/281] Add test coverage of previous failcase --- .../Formats/LegacyStoryboardDecoderTest.cs | 18 ++++++++++++++++++ ...-fade-transform-is-ignored-for-lifetime.osb | 8 ++++++++ 2 files changed, 26 insertions(+) create mode 100644 osu.Game.Tests/Resources/noop-fade-transform-is-ignored-for-lifetime.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 647c0aed75..821173c521 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -135,6 +135,24 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestNoopFadeTransformIsIgnoredForLifetime() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("noop-fade-transform-is-ignored-for-lifetime.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(2, background.Elements.Count); + + Assert.AreEqual(1500, background.Elements[0].StartTime); + Assert.AreEqual(1500, background.Elements[1].StartTime); + } + } + [Test] public void TestOutOfOrderStartTimes() { diff --git a/osu.Game.Tests/Resources/noop-fade-transform-is-ignored-for-lifetime.osb b/osu.Game.Tests/Resources/noop-fade-transform-is-ignored-for-lifetime.osb new file mode 100644 index 0000000000..aca9bf926a --- /dev/null +++ b/osu.Game.Tests/Resources/noop-fade-transform-is-ignored-for-lifetime.osb @@ -0,0 +1,8 @@ +[Events] +//Storyboard Layer 0 (Background) +Sprite,Background,TopCentre,"img.jpg",320,240 + F,0,1000,1000,0,0 // should be ignored + F,0,1500,1600,0,1 +Sprite,Background,TopCentre,"img.jpg",320,240 + F,0,1000,1000,0,0 // should be ignored + F,0,1500,1600,1,1 From f3524ad8f545efe40f63374a7b1a6a2b26220db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Mar 2025 15:00:34 +0100 Subject: [PATCH 023/281] Fix daily challenge not querying beatmap properly Noticed during review of https://github.com/ppy/osu/pull/32571. The reproduction scenario is as follows: 1. Download beatmap used in daily challenge 2. Go to editor and modify it 3. Go to daily challenge, wherein the availability tracker will notice the MD5 mismatch, block the button, and require a redownload 4. Redownload the beatmap 5. Start gameplay 6. Gameplay start will fail due to web not issuing a submission token because the attempt to start gameplay ended up using the modified version of the map from step (2) rather than the redownloaded original from step (4). Thankfully, due to (6), this is not exploitable, but nevertheless pretty bad. Probably regressed somewhere around https://github.com/ppy/osu/pull/31747 actually. --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index cb881ccfe5..43db586c45 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -490,7 +490,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (!screen.IsCurrentScreen()) return; - var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID); + var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID && b.MD5Hash == item.Beatmap.MD5Hash); screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. screen.Ruleset.Value = rulesets.GetRuleset(item.RulesetID); From bb8f8e8d8c709069d41338afb01206869dee4c99 Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:41:48 +0100 Subject: [PATCH 024/281] Use median instead of mean for automatic beatmap offset adjustment --- .../Rulesets/Scoring/HitEventExtensions.cs | 19 +++++++++++++++++++ .../PlayerSettings/BeatmapOffsetControl.cs | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index fed0c3b51b..da1ac9f2a1 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -71,6 +71,25 @@ namespace osu.Game.Rulesets.Scoring return timeOffsets.Average(); } + /// + /// Calculates the median hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. + /// + /// + /// A non-null value if unstable rate could be calculated, + /// and if unstable rate cannot be calculated due to being empty. + /// + public static double? CalculateMedianHitError(this IEnumerable hitEvents) + { + double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).OrderBy(x => x).ToArray(); + + if (timeOffsets.Length == 0) + return null; + + int center = timeOffsets.Length / 2; + + return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center]; + } + public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result); public static bool AffectsUnstableRate(HitObject hitObject, HitResult result) => hitObject.HitWindows != HitWindows.Empty && result.IsHit(); diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index cef5884d39..ce474ed594 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -196,7 +196,7 @@ namespace osu.Game.Screens.Play.PlayerSettings var hitEvents = score.NewValue.HitEvents; - if (!(hitEvents.CalculateAverageHitError() is double average)) + if (!(hitEvents.CalculateMedianHitError() is double median)) return; referenceScoreContainer.Children = new Drawable[] @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.PlayerSettings return; } - lastPlayAverage = average; + lastPlayAverage = median; lastPlayBeatmapOffset = Current.Value; LinkFlowContainer globalOffsetText; From f907758b8b5f07c63443e3ae0b912f00b9bcd657 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 00:11:55 +0900 Subject: [PATCH 025/281] Fix room background not working in multiplayer At the core of the problem is that the multiplayer server does not serialise beatmap covers to clients -- only the beatmap ids. Because of this, any components that need to display the background should query it from an online source first (i.e. via `BeatmapLookupCache`). There is a slightly tricky situation here formed of two parts, which I'll try to explain below. `Background.Sprite` is exposed publicly and some inheritors override the sprite's texture in a similar fashion to the way this changeset does. While I frankly believe this is unnaceptable from an encapsulation point of view, I've gone for consistency in this regard. The other fail case is `UpdateableBeatmapBackgroundSprite`. Contrary to its name, that object is _not_ a `Sprite` - it is a `ModelBackedDrawable` that _contains_ a `Sprite`. The logic in this PR could be extracted into a separate object similar to `OnlineBeatmapSetCover` (an actual `Sprite`), but even so it would require `Background` to provide a path for overriding its contained `Sprite`. I went through the path above originally with the changes visible in https://github.com/smoogipoo/osu/tree/fix-mp-backgrounds, but it looks quite daunting and I feel like there would be a lot of emphasis on the correct abstraction model for `Background`, of which I'm not entirely sure of. Instead of dealing with both of the above, this commit presents a direct; local resolution to the problem. --- .../Components/OnlinePlayBackgroundScreen.cs | 113 ++++++++++++------ .../Components/PlaylistItemBackground.cs | 42 ------- 2 files changed, 76 insertions(+), 79 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs index ef7c1747e9..4ca2d8e8a6 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs @@ -3,10 +3,15 @@ using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Textures; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osuTK; using osuTK.Graphics; @@ -16,65 +21,57 @@ namespace osu.Game.Screens.OnlinePlay.Components public abstract partial class OnlinePlayBackgroundScreen : BackgroundScreen { private CancellationTokenSource? cancellationSource; - private PlaylistItemBackground? background; + private Background? lastBackground; + private int? beatmapId; [BackgroundDependencyLoader] private void load() { - switchBackground(new PlaylistItemBackground(playlistItem)); + loadNewBackground(); } - private PlaylistItem? playlistItem; - protected PlaylistItem? PlaylistItem { - get => playlistItem; set { - if (playlistItem == value) + if (beatmapId == value?.Beatmap.OnlineID) return; - playlistItem = value; + beatmapId = value?.Beatmap.OnlineID; - if (LoadState > LoadState.Ready) - updateBackground(); + if (LoadState >= LoadState.Ready) + loadNewBackground(); } } - private void updateBackground() + private void loadNewBackground() { - Schedule(() => + cancellationSource?.Cancel(); + cancellationSource = new CancellationTokenSource(); + + if (beatmapId == null) + switchBackground(new DefaultBackground()); + else + LoadComponentAsync(new OnlineBeatmapBackground(beatmapId.Value), switchBackground, cancellationSource.Token); + + void switchBackground(Background newBackground) { - var beatmap = playlistItem?.Beatmap; + float newDepth = 0; - string? lastCover = (background?.Beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.Covers.Cover; - string? newCover = (beatmap?.BeatmapSet as IBeatmapSetOnlineInfo)?.Covers.Cover; + if (lastBackground != null) + { + newDepth = lastBackground.Depth + 1; + lastBackground.FinishTransforms(); + lastBackground.FadeOut(250); + lastBackground.Expire(); + } - if (lastCover == newCover) - return; + newBackground.Depth = newDepth; + newBackground.Colour = ColourInfo.GradientVertical(new Color4(0.1f, 0.1f, 0.1f, 1f), new Color4(0.4f, 0.4f, 0.4f, 1f)); + newBackground.BlurTo(new Vector2(10)); - cancellationSource?.Cancel(); - LoadComponentAsync(new PlaylistItemBackground(playlistItem), switchBackground, (cancellationSource = new CancellationTokenSource()).Token); - }); - } - - private void switchBackground(PlaylistItemBackground newBackground) - { - float newDepth = 0; - - if (background != null) - { - newDepth = background.Depth + 1; - background.FinishTransforms(); - background.FadeOut(250); - background.Expire(); + AddInternal(lastBackground = newBackground); } - - newBackground.Depth = newDepth; - newBackground.Colour = ColourInfo.GradientVertical(new Color4(0.1f, 0.1f, 0.1f, 1f), new Color4(0.4f, 0.4f, 0.4f, 1f)); - newBackground.BlurTo(new Vector2(10)); - - AddInternal(background = newBackground); } public override void OnSuspending(ScreenTransitionEvent e) @@ -89,5 +86,47 @@ namespace osu.Game.Screens.OnlinePlay.Components this.MoveToX(0); return result; } + + [LongRunningLoad] + private partial class OnlineBeatmapBackground : Background + { + private readonly int beatmapId; + + public OnlineBeatmapBackground(int beatmapId) + { + this.beatmapId = beatmapId; + } + + [BackgroundDependencyLoader] + private void load(BeatmapLookupCache lookupCache, LargeTextureStore textures, CancellationToken cancellationToken) + { + try + { + APIBeatmap? beatmap = lookupCache.GetBeatmapAsync(beatmapId, cancellationToken).GetResultSafely(); + string? coverImage = beatmap?.BeatmapSet?.Covers.Cover; + + if (coverImage != null) + Sprite.Texture = textures.Get(coverImage); + } + catch + { + } + } + } + + private class DefaultBackground : Background + { + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Sprite.Texture = beatmapManager.DefaultBeatmap.GetBackground(); + } + + public override bool Equals(Background? other) + => other is DefaultBackground; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs b/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs deleted file mode 100644 index 6b06eaee1e..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/PlaylistItemBackground.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Textures; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Online.Rooms; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - public partial class PlaylistItemBackground : Background - { - public readonly IBeatmapInfo? Beatmap; - - public PlaylistItemBackground(PlaylistItem? playlistItem) - { - Beatmap = playlistItem?.Beatmap; - } - - [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, LargeTextureStore textures) - { - Texture? texture = null; - - // prefer online cover where available. - if (Beatmap?.BeatmapSet is IBeatmapSetOnlineInfo online) - texture = textures.Get(online.Covers.Cover); - - Sprite.Texture = texture ?? beatmaps.DefaultBeatmap.GetBackground(); - } - - public override bool Equals(Background? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - - return other.GetType() == GetType() - && ((PlaylistItemBackground)other).Beatmap == Beatmap; - } - } -} From c720f2b33f1bde123d4ba0240be6605ac445f83a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 00:35:21 +0900 Subject: [PATCH 026/281] Make class partial --- .../Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs index 4ca2d8e8a6..4b34987c30 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.OnlinePlay.Components } } - private class DefaultBackground : Background + private partial class DefaultBackground : Background { [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; From 6452514066161dd6c38e533c5564f6c0249abb59 Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:22:57 +0100 Subject: [PATCH 027/281] Add comment --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index da1ac9f2a1..01d800a351 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -87,6 +87,7 @@ namespace osu.Game.Rulesets.Scoring int center = timeOffsets.Length / 2; + // Use average of the 2 central values if length is even return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center]; } From 77d73c5f50ef2a36877f0a1b33e0cdab356ead98 Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:23:19 +0100 Subject: [PATCH 028/281] Increase number of timed hits needed to activate button --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index ce474ed594..c2cd09c56f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play.PlayerSettings // affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event, // i.e. an user input that the user had to *time to the track*, // i.e. one that it *makes sense to use* when doing anything with timing and offsets. - if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 10) + if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 50) { referenceScoreContainer.AddRange(new Drawable[] { From 4b8fe015e56614b1a98c3e2081ff9606d0d3bd9b Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:53:03 +0100 Subject: [PATCH 029/281] Apply median to `SessionAverageHitErrorTracker` --- osu.Game/Configuration/SessionAverageHitErrorTracker.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs index cd21eb6fa8..49f7657f91 100644 --- a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs +++ b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs @@ -40,10 +40,10 @@ namespace osu.Game.Configuration if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs)) return; - if (newScore.HitEvents.Count < 10) + if (newScore.HitEvents.Count < 50) return; - if (newScore.HitEvents.CalculateAverageHitError() is not double averageError) + if (newScore.HitEvents.CalculateMedianHitError() is not double medianError) return; // keep a sane maximum number of entries. @@ -51,7 +51,7 @@ namespace osu.Game.Configuration averageHitErrorHistory.RemoveAt(0); double globalOffset = configManager.Get(OsuSetting.AudioOffset); - averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset)); + averageHitErrorHistory.Add(new DataPoint(medianError, globalOffset)); } public void ClearHistory() => averageHitErrorHistory.Clear(); From 1c4ecba950beab9662638561b4752e157f82ebab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Mar 2025 08:50:11 +0100 Subject: [PATCH 030/281] Fix several issues with multiplayer & playlists room join error logging This is in response to https://osu.ppy.sh/community/forums/topics/2058708?n=5, wherein the user is having a problem with joining multiplayer, but I have basically no diagnosing capabilities, because the logs are all 2025-03-26 18:57:57 [error]: Failed to join multiplayer room: 2025-03-26 18:58:40 [error]: Failed to join multiplayer room: 2025-03-26 18:58:41 [error]: Failed to join multiplayer room: 2025-03-26 18:58:41 [error]: Failed to join multiplayer room: which appears to originate from https://github.com/ppy/osu/blob/c82eaafe98d96b9f49a4a7f168ef5c484e67d76f/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs#L91 Now, as far as I can tell, there are two possibilities here: 1. The exception's `Message` is null or empty. That's not exactly great or typical, but sure, this could possibly happen - in which case the error logging is silently eating whatever little relevant detail there is left to use. 2. The exception is *actually* `null` itself, and we're in the X Files. This PR is intending to defend against (1). In examining the logging further, I also spotted the following issues: - In the single path that specifies a custom failure handler (which is `DrawableLoungeRoom` which handles joining a passworded room), the custom failure handler being present means that the error would be presented to the user, but it would not be logged. At all. - In playlists, if the exception for whatever reason had an empty message, an empty notification would get posted. And in general to me it feels a bit dodgy to be directly presenting exception notifications to users without any preamble, hence the added "Failed to open playlist" prefix. --- .../Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs | 2 +- .../Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs | 6 ++++-- .../Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 10 +++++----- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 6 +++--- .../OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs | 4 ++-- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs index 459a90d096..5d6bc5e50a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { var mockLounge = new Mock(); mockLounge - .Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>())) + .Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>())) .Callback, Action>((_, _, _, d) => { Task.Run(() => diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index d369722e5f..339780b517 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -17,6 +17,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -306,13 +307,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge GetContainingFocusManager()?.TriggerFocusContention(passwordTextBox); } - private void joinFailed(string error) => Schedule(() => + private void joinFailed(string message, Exception? exception) => Schedule(() => { passwordTextBox.Text = string.Empty; GetContainingFocusManager()!.ChangeFocus(passwordTextBox); - errorText.Text = error; + Logger.Log($"Failed to join room with password. {exception}"); + errorText.Text = message; errorText .FadeIn() .FlashColour(Color4.White, 200) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs index 73ab84af13..c9f6921328 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/IOnlinePlayLounge.cs @@ -14,8 +14,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge /// The room to join. /// The password. /// A delegate to invoke if the user joined the room. - /// A delegate to invoke if the user is not able join the room. - void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null); + /// A delegate to invoke if the user is not able to join the room. + void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null); /// /// Copies the given room and opens it as a fresh (not-yet-created) one. diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index c455020f9a..a4e808ff76 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -334,7 +334,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge popoverContainer.HidePopover(); } - public void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() => + public void Join(Room room, string? password, Action? onSuccess = null, Action? onFailure = null) => Schedule(() => { if (joiningRoomOperation != null) return; @@ -347,19 +347,19 @@ namespace osu.Game.Screens.OnlinePlay.Lounge joiningRoomOperation?.Dispose(); joiningRoomOperation = null; onSuccess?.Invoke(room); - }, error => + }, (message, exception) => { joiningRoomOperation?.Dispose(); joiningRoomOperation = null; if (onFailure != null) - onFailure(error); + onFailure(message, exception); else - Logger.Log(error, level: LogLevel.Error); + Logger.Error(exception, message); }); }); - protected abstract void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure); + protected abstract void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure); public void OpenCopy(Room room) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 51c135f042..5e2619eae3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override OnlinePlaySubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); - protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) + protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { client.JoinRoom(room, password).ContinueWith(result => { @@ -86,9 +86,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Exception? exception = result.Exception?.AsSingular(); if (exception?.GetHubExceptionMessage() is string message) - onFailure(message); + onFailure(message, exception); else - onFailure($"Failed to join multiplayer room: {exception?.Message}"); + onFailure($"Failed to join multiplayer room. {exception?.Message}", exception); } }); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index eccbaf7930..cc4065a82b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists return criteria; } - protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) + protected override void JoinInternal(Room room, string? password, Action onSuccess, Action onFailure) { var joinRoomRequest = new JoinRoomRequest(room, password); @@ -68,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists joinRoomRequest.Failure += exception => { if (exception is not OperationCanceledException) - onFailure(exception.Message); + onFailure($"Failed to open playlist. {exception.Message}", exception); }; api.Queue(joinRoomRequest); From f73601535d4a1a1a39bb989a11f942a7e46bd6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Mar 2025 09:33:35 +0100 Subject: [PATCH 031/281] Use alternative method of fixing issue of improper beatmap query in daily challenge (and expand to other online play modes) --- osu.Game/Beatmaps/BeatmapManager.cs | 16 +++++++++++++++- .../OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Playlists/PlaylistsRoomSubScreen.cs | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 1e66b28b15..2c17908487 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -298,7 +298,21 @@ namespace osu.Game.Beatmaps /// The query. /// The first result for the provided query, or null if no results were found. public BeatmapInfo? QueryBeatmap(Expression> query) => Realm.Run(r => - r.All().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); + r.All().Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach()); + + /// + /// Perform a lookup query on available s. + /// Use this overload instead of + /// when Realm is unable to transform an expression to the internal Realm query syntax. + /// + /// The query. + /// The arguments for the query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo? QueryBeatmap(string query, params QueryArgument[] arguments) => Realm.Run(r => + r.All() + .Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false") + .Filter(query, arguments) + .FirstOrDefault()?.Detach()); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 43db586c45..171524870f 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -490,7 +490,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (!screen.IsCurrentScreen()) return; - var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID && b.MD5Hash == item.Beatmap.MD5Hash); + var beatmap = beatmaps.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", item.Beatmap.OnlineID); screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. screen.Ruleset.Value = rulesets.GetRuleset(item.RulesetID); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index eca59c8393..39cdaaf2e9 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -456,7 +456,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 89416e66bf..6aa93ca2ba 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -598,7 +598,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // Update global gameplay state to correspond to the new selection. // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = gameplayBeatmap.OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = gameplayRuleset; Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); From 094a22a4a0de146575849db1a2320d0998a1c38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Mar 2025 09:46:14 +0100 Subject: [PATCH 032/281] Use `==` consistently --- osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs | 2 +- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 171524870f..893bc4eb5c 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -490,7 +490,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (!screen.IsCurrentScreen()) return; - var beatmap = beatmaps.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", item.Beatmap.OnlineID); + var beatmap = beatmaps.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.Beatmap.OnlineID); screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally. screen.Ruleset.Value = rulesets.GetRuleset(item.RulesetID); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 39cdaaf2e9..6c8043d8bb 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -456,7 +456,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 6aa93ca2ba..b13a418276 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -598,7 +598,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // Update global gameplay state to correspond to the new selection. // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = gameplayBeatmap.OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} = $0 AND {nameof(BeatmapInfo.MD5Hash)} = {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = gameplayRuleset; Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray(); From 2cfb06bcde85d7f9cf63329145c7a2e484b76a29 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 17:49:21 +0900 Subject: [PATCH 033/281] Fix test --- .../Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs index 5d6bc5e50a..5875ad74e7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Lounge; @@ -41,13 +42,13 @@ namespace osu.Game.Tests.Visual.Multiplayer var mockLounge = new Mock(); mockLounge .Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>())) - .Callback, Action>((_, _, _, d) => + .Callback, Action>((_, _, _, d) => { Task.Run(() => { allowResponseCallback.Wait(10000); allowResponseCallback.Reset(); - Schedule(() => d?.Invoke("Incorrect password")); + Schedule(() => d?.Invoke("Incorrect password", new InvalidPasswordException())); }); }); From 1f77ef554443298ad0bfdd6c8753211f6df5de09 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 17:53:45 +0900 Subject: [PATCH 034/281] Resolve inspection --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index b13a418276..1fefdb350c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -597,7 +597,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // Update global gameplay state to correspond to the new selection. // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - int beatmapId = gameplayBeatmap.OnlineID; var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = gameplayRuleset; From 8c244134d536339a19556c30abe2f6fe307f8846 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 19:05:11 +0900 Subject: [PATCH 035/281] Add failing test --- .../TestSceneModSelectOverlay.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 499b28fb49..017d246461 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -1003,6 +1003,35 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("search still not focused", () => !this.ChildrenOfType().Single().HasFocus); } + /// + /// Tests that recreating the mod panels (by setting the global available mods) also refreshes the active states. + /// + [Test] + public void TestActiveStatesRefreshedOnPanelsCreated() + { + createScreen(); + changeRuleset(0); + + Bindable> selectedMods = null!; + + AddStep("bind mods to local bindable", () => + { + selectedMods = new Bindable>([]); + + modSelectOverlay.SelectedMods.UnbindFrom(SelectedMods); + modSelectOverlay.SelectedMods.BindTo(selectedMods); + }); + + AddStep("activate PF", () => selectedMods.Value = [new OsuModPerfect()]); + AddAssert("OsuModPerfect panel active", () => getPanelForMod(typeof(OsuModPerfect)).Active.Value); + + changeRuleset(1); + AddAssert("TaikoModPerfect panel not active", () => !getPanelForMod(typeof(TaikoModPerfect)).Active.Value); + + changeRuleset(0); + AddAssert("OsuModPerfect panel active", () => getPanelForMod(typeof(OsuModPerfect)).Active.Value); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded) From dcb5389ab750956e773727ffdd76caf463cbaecd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 19:05:26 +0900 Subject: [PATCH 036/281] Refresh mod panel active states when recreated --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ac589fbebf..d36092ebed 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -353,7 +353,10 @@ namespace osu.Game.Overlays.Mods .ToArray(); foreach (var modState in modStates) + { + modState.Active.Value = SelectedMods.Value.Any(selected => selected.GetType() == modState.Mod.GetType()); modState.Active.BindValueChanged(_ => updateFromInternalSelection()); + } newLocalAvailableMods[modType] = modStates; } From 050cc27125c5b23f5134e3e753a951d4e6876ef0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 19:24:57 +0900 Subject: [PATCH 037/281] Apply suggestions from review --- .../Components/OnlinePlayBackgroundScreen.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs index 4b34987c30..4f9d1b9246 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundScreen.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Database; @@ -108,9 +110,13 @@ namespace osu.Game.Screens.OnlinePlay.Components if (coverImage != null) Sprite.Texture = textures.Get(coverImage); } - catch + catch (OperationCanceledException) { } + catch (Exception ex) + { + Logger.Error(ex, $"Failed to retrieve cover image for beatmap {beatmapId}."); + } } } @@ -124,9 +130,6 @@ namespace osu.Game.Screens.OnlinePlay.Components { Sprite.Texture = beatmapManager.DefaultBeatmap.GetBackground(); } - - public override bool Equals(Background? other) - => other is DefaultBackground; } } } From 2ba621550c840aa9e92c917338b744b381df6752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Mar 2025 12:39:19 +0100 Subject: [PATCH 038/281] Fix tests --- .../Visual/Multiplayer/QueueModeTestScene.cs | 5 +++++ .../Visual/Multiplayer/TestSceneMultiplayer.cs | 5 +++++ .../Playlists/TestScenePlaylistsRoomCreation.cs | 6 ++++++ .../Playlists/TestScenePlaylistsRoomSubScreen.cs | 12 ++++++++---- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index 0e01751d76..0e8093f459 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -61,6 +61,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + Realm.Write(r => + { + foreach (var beatmapInfo in r.All()) + beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; + }); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); InitialBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0); OtherBeatmap = importedSet.Beatmaps.Last(b => b.Ruleset.OnlineID == 0); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index ae939c7b9e..a62d9676c2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -81,6 +81,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("import beatmap", () => { beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + Realm.Write(r => + { + foreach (var beatmapInfo in r.All()) + beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; + }); importedSet = beatmaps.GetAllUsableBeatmapSets().First(); }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index a748d61d44..2e90f08d47 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -176,6 +176,7 @@ namespace osu.Game.Tests.Visual.Playlists RulesetID = new OsuRuleset().RulesetInfo.OnlineID } ]; + room.EndDate = DateTimeOffset.Now.AddHours(1); }); AddAssert("match has default beatmap", () => match.Beatmap.IsDefault); @@ -212,6 +213,11 @@ namespace osu.Game.Tests.Visual.Playlists Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null); importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach(); + Realm.Write(r => + { + foreach (var beatmapInfo in r.All()) + beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; + }); }); private partial class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index e9c4b56e04..1841e2fd52 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -65,7 +65,8 @@ namespace osu.Game.Tests.Visual.Playlists OnlineID = 1, DifficultyName = "Osu 1", Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = "11111111", + OnlineMD5Hash = "11111111", Ruleset = new OsuRuleset().RulesetInfo, Metadata = { @@ -79,7 +80,8 @@ namespace osu.Game.Tests.Visual.Playlists OnlineID = 2, DifficultyName = "Osu 2", Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = "22222222", + OnlineMD5Hash = "22222222", Ruleset = new OsuRuleset().RulesetInfo, Metadata = { @@ -93,7 +95,8 @@ namespace osu.Game.Tests.Visual.Playlists OnlineID = 3, DifficultyName = "Taiko 1", Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = "33333333", + OnlineMD5Hash = "33333333", Ruleset = new TaikoRuleset().RulesetInfo, Metadata = { @@ -107,7 +110,8 @@ namespace osu.Game.Tests.Visual.Playlists OnlineID = 4, DifficultyName = "Taiko 2", Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), - MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + MD5Hash = "44444444", + OnlineMD5Hash = "44444444", Ruleset = new TaikoRuleset().RulesetInfo, Metadata = { diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 1f0c08714e..caf99a4cf6 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -68,6 +68,7 @@ namespace osu.Game.Tests.Beatmaps var b = Decoder.GetDecoder(reader).Decode(reader); b.BeatmapInfo.MD5Hash = test_beatmap_hash.Value.md5; + b.BeatmapInfo.OnlineMD5Hash = test_beatmap_hash.Value.md5; b.BeatmapInfo.Hash = test_beatmap_hash.Value.sha2; return b; From 3c479032f1ed59faa17efb2c745465a1e8fbedef Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 20:44:56 +0900 Subject: [PATCH 039/281] Remove unused bindables --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index b9fe45d227..fc8fb4c544 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -51,10 +51,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected readonly Bindable SelectedItem = new Bindable(); protected Container ButtonsContainer { get; private set; } = null!; - private readonly Bindable roomType = new Bindable(); - private readonly Bindable roomCategory = new Bindable(); - private readonly Bindable hasPassword = new Bindable(); - private DrawableRoomParticipantsList? drawableRoomParticipantsList; private RoomSpecialCategoryPill? specialCategoryPill; private PasswordProtectedIcon? passwordIcon; From d43beeaf55f19b5e15d4853e9b073d04cf0d0800 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 27 Mar 2025 21:08:05 +0900 Subject: [PATCH 040/281] Fix background not showing in multiplayer room panel --- .../OnlinePlay/Lounge/Components/RoomPanel.cs | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index fc8fb4c544..6aeaf01c45 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -46,6 +46,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private OsuGame? game { get; set; } + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; + public readonly Room Room; protected readonly Bindable SelectedItem = new Bindable(); @@ -56,8 +59,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private PasswordProtectedIcon? passwordIcon; private EndDateInfo? endDateInfo; private SpriteText? roomName; - private UpdateableBeatmapBackgroundSprite background = null!; private DelayedLoadWrapper wrapper = null!; + private CancellationTokenSource? beatmapLookupCancellation; + + /// + /// A fully-populated representation of the selected item's current beatmap. + /// + private readonly Bindable currentBeatmap = new Bindable(); protected RoomPanel(Room room) { @@ -95,9 +103,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Colour = colours.Background5, }, - background = CreateBackground().With(d => + CreateBackground().With(d => { d.RelativeSizeAxes = Axes.Both; + d.Beatmap.BindTarget = currentBeatmap; }), wrapper = new DelayedLoadWrapper(() => new Container @@ -202,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, new RoomStatusText(Room) { - SelectedItem = { BindTarget = SelectedItem } + Beatmap = { BindTarget = currentBeatmap } } } } @@ -276,7 +285,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components updateRoomHasPassword(); }; - SelectedItem.BindValueChanged(item => background.Beatmap.Value = item.NewValue?.Beatmap, true); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); } private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -301,6 +310,30 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private void onSelectedItemChanged(ValueChangedEvent item) + { + if (item.NewValue?.Beatmap.OnlineID == item.OldValue?.Beatmap.OnlineID) + return; + + beatmapLookupCancellation?.Cancel(); + beatmapLookupCancellation?.Dispose(); + + if (item.NewValue?.Beatmap == null) + { + currentBeatmap.Value = null; + return; + } + + var cancellationSource = beatmapLookupCancellation = new CancellationTokenSource(); + + beatmapLookupCache.GetBeatmapAsync(item.NewValue.Beatmap.OnlineID, cancellationSource.Token) + .ContinueWith(task => Schedule(() => + { + if (!cancellationSource.IsCancellationRequested) + currentBeatmap.Value = task.GetResultSafely(); + }), cancellationSource.Token); + } + private void updateRoomName() { if (roomName != null) @@ -402,14 +435,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private partial class RoomStatusText : CompositeDrawable { - public readonly IBindable SelectedItem = new Bindable(); + public readonly Bindable Beatmap = new Bindable(); [Resolved] private OsuColour colours { get; set; } = null!; - [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; - private readonly Room room; private SpriteText statusText = null!; private LinkFlowContainer beatmapText = null!; @@ -465,14 +495,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components protected override void LoadComplete() { base.LoadComplete(); - SelectedItem.BindValueChanged(onSelectedItemChanged, true); + + Beatmap.BindValueChanged(onBeatmapChanged, true); } - private CancellationTokenSource? beatmapLookupCancellation; - - private void onSelectedItemChanged(ValueChangedEvent item) + private void onBeatmapChanged(ValueChangedEvent beatmap) { - beatmapLookupCancellation?.Cancel(); beatmapText.Clear(); if (room.Type == MatchType.Playlists) @@ -481,31 +509,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return; } - var beatmap = item.NewValue?.Beatmap; - if (beatmap == null) - return; + statusText.Text = "Currently playing "; - var cancellationSource = beatmapLookupCancellation = new CancellationTokenSource(); - beatmapLookupCache.GetBeatmapAsync(beatmap.OnlineID, cancellationSource.Token) - .ContinueWith(task => Schedule(() => - { - if (cancellationSource.IsCancellationRequested) - return; - - var retrievedBeatmap = task.GetResultSafely(); - - statusText.Text = "Currently playing "; - - if (retrievedBeatmap != null) - { - beatmapText.AddLink(retrievedBeatmap.GetDisplayTitleRomanisable(), - LinkAction.OpenBeatmap, - retrievedBeatmap.OnlineID.ToString(), - creationParameters: s => s.Truncate = true); - } - else - beatmapText.AddText("unknown beatmap"); - }), cancellationSource.Token); + if (beatmap.NewValue != null) + { + beatmapText.AddLink(beatmap.NewValue.GetDisplayTitleRomanisable(), + LinkAction.OpenBeatmap, + beatmap.NewValue.OnlineID.ToString(), + creationParameters: s => s.Truncate = true); + } + else + beatmapText.AddText("unknown beatmap"); } } From 7bb97d57966e71394c549777deb8396f1c5c79c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Mar 2025 15:01:36 +0100 Subject: [PATCH 041/281] Show user tags on beatmap set overlay Resolves one part of https://github.com/ppy/osu/discussions/32568#discussioncomment-12612928 A few caveats: - Layout is slightly different than web intentionally. Web does things that I think will be difficult to reproduce or just plain look bad in client, such as: - On web, the metadata info box has 200px min height and 300px max height. I just hardcoded 300 units. - On web, user tags and mapper tags are individually scrollable, and the amount of space taken up by each is calculated in a way that is - as far as I can tell - indeterminate, and probably influenced by some flexbox magic. I just made the entire thing scrollable instead. - Because song select shares controls with the beatmap set overlay, now song select says "Mapper Tags" in the header instead of just "Tags" too. I think this is fine, because people asked for user tags to be shown in song select too. - Search query syntax lifted from https://github.com/ppy/osu-web/pull/12047. - Using hardcoded English strings for now, will update to the translations after the next osu-resources localisations update. --- .../Online/TestSceneBeatmapSetOverlay.cs | 27 +++++++++ .../API/Requests/Responses/APIBeatmapSet.cs | 3 + osu.Game/Overlays/BeatmapSet/Info.cs | 60 +++++++++++++------ ...onTags.cs => MetadataSectionMapperTags.cs} | 6 +- .../BeatmapSet/MetadataSectionUserTags.cs | 39 ++++++++++++ osu.Game/Overlays/BeatmapSet/MetadataType.cs | 8 ++- osu.Game/Overlays/BeatmapSetOverlay.cs | 11 ++-- osu.Game/Screens/Select/BeatmapDetails.cs | 2 +- 8 files changed, 127 insertions(+), 29 deletions(-) rename osu.Game/Overlays/BeatmapSet/{MetadataSectionTags.cs => MetadataSectionMapperTags.cs} (80%) create mode 100644 osu.Game/Overlays/BeatmapSet/MetadataSectionUserTags.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 822e5f26bd..5dc6f950a5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -99,8 +99,35 @@ namespace osu.Game.Tests.Visual.Online Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), }, + TopTags = + [ + new APIBeatmapTag { TagId = 4, VoteCount = 1 }, + new APIBeatmapTag { TagId = 2, VoteCount = 1 }, + new APIBeatmapTag { TagId = 23, VoteCount = 5 }, + ], }, }, + RelatedTags = + [ + new APITag + { + Id = 2, + Name = "song representation/simple", + Description = "Accessible and straightforward map design." + }, + new APITag + { + Id = 4, + Name = "style/clean", + Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects." + }, + new APITag + { + Id = 23, + Name = "aim/aim control", + Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern." + } + ] }); }); diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index d98715a42d..c6cf0f735f 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -128,6 +128,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"converts")] public APIBeatmap[]? Converts { get; set; } + [JsonProperty(@"related_tags")] + public APITag[]? RelatedTags { get; set; } + private BeatmapMetadata metadata => new BeatmapMetadata { Title = Title, diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index d21b2546b9..37741b63ce 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -9,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapListing; @@ -17,26 +20,22 @@ namespace osu.Game.Overlays.BeatmapSet { public partial class Info : Container { - private const float metadata_width = 175; + private const float metadata_width = 185; private const float spacing = 20; - private const float base_height = 220; + private const float base_height = 300; private readonly Box successRateBackground; private readonly Box background; - private readonly SuccessRate successRate; + private readonly MetadataSection userTags; public readonly Bindable BeatmapSet = new Bindable(); - - public APIBeatmap? BeatmapInfo - { - get => successRate.Beatmap; - set => successRate.Beatmap = value; - } + public readonly Bindable Beatmap = new Bindable(); public Info() { + SuccessRate successRate; MetadataSectionNominators nominators; - MetadataSection source, tags; + MetadataSection source, mapperTags; MetadataSectionGenre genre; MetadataSectionLanguage language; OsuSpriteText notRankedPlaceholder; @@ -66,27 +65,30 @@ namespace osu.Game.Overlays.BeatmapSet Child = new MetadataSectionDescription(), }, }, - new Container + new OsuScrollContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = metadata_width, - Padding = new MarginPadding { Horizontal = 10 }, + Padding = new MarginPadding { Left = 10 }, Margin = new MarginPadding { Right = BeatmapSetOverlay.RIGHT_WIDTH + spacing }, Masking = true, + ScrollbarOverlapsContent = false, Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, + Padding = new MarginPadding { Right = 5 }, Children = new Drawable[] { nominators = new MetadataSectionNominators(), source = new MetadataSectionSource(), genre = new MetadataSectionGenre { Width = 0.5f }, language = new MetadataSectionLanguage { Width = 0.5f }, - tags = new MetadataSectionTags(), + userTags = new MetadataSectionUserTags(), + mapperTags = new MetadataSectionMapperTags(), }, }, }, @@ -121,18 +123,42 @@ namespace osu.Game.Overlays.BeatmapSet }, }; - BeatmapSet.ValueChanged += b => + BeatmapSet.BindValueChanged(b => { nominators.Metadata = (b.NewValue?.CurrentNominations ?? Array.Empty(), b.NewValue?.RelatedUsers ?? Array.Empty()); source.Metadata = b.NewValue?.Source ?? string.Empty; - tags.Metadata = b.NewValue?.Tags ?? string.Empty; + mapperTags.Metadata = b.NewValue?.Tags ?? string.Empty; + updateUserTags(); genre.Metadata = b.NewValue?.Genre ?? new BeatmapSetOnlineGenre { Id = (int)SearchGenre.Unspecified }; language.Metadata = b.NewValue?.Language ?? new BeatmapSetOnlineLanguage { Id = (int)SearchLanguage.Unspecified }; bool setHasLeaderboard = b.NewValue?.Status > 0; successRate.Alpha = setHasLeaderboard ? 1 : 0; notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1; - Height = setHasLeaderboard ? 270 : base_height; - }; + }); + Beatmap.BindValueChanged(b => + { + successRate.Beatmap = b.NewValue; + updateUserTags(); + }); + } + + private void updateUserTags() + { + if (Beatmap.Value?.TopTags == null || Beatmap.Value.TopTags.Length == 0 || BeatmapSet.Value?.RelatedTags == null) + { + userTags.Metadata = null; + return; + } + + var tagsById = BeatmapSet.Value.RelatedTags.ToDictionary(t => t.Id); + userTags.Metadata = Beatmap.Value.TopTags + .Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId))) + .Where(t => t.relatedTag != null) + // see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria + .OrderByDescending(t => t.topTag.VoteCount) + .ThenBy(t => t.relatedTag!.Name) + .Select(t => t.relatedTag!.Name) + .ToArray(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionMapperTags.cs similarity index 80% rename from osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs rename to osu.Game/Overlays/BeatmapSet/MetadataSectionMapperTags.cs index fc16ba19d8..47e839a84d 100644 --- a/osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs +++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionMapperTags.cs @@ -7,10 +7,10 @@ using osu.Game.Online.Chat; namespace osu.Game.Overlays.BeatmapSet { - public partial class MetadataSectionTags : MetadataSection + public partial class MetadataSectionMapperTags : MetadataSection { - public MetadataSectionTags(Action? searchAction = null) - : base(MetadataType.Tags, searchAction) + public MetadataSectionMapperTags(Action? searchAction = null) + : base(MetadataType.MapperTags, searchAction) { } diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSectionUserTags.cs b/osu.Game/Overlays/BeatmapSet/MetadataSectionUserTags.cs new file mode 100644 index 0000000000..3a9fe8d33f --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/MetadataSectionUserTags.cs @@ -0,0 +1,39 @@ +// 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.Game.Graphics.Containers; +using osu.Game.Online.Chat; + +namespace osu.Game.Overlays.BeatmapSet +{ + public partial class MetadataSectionUserTags : MetadataSection + { + private readonly Action? searchAction; + + public MetadataSectionUserTags(Action? searchAction = null) + : base(MetadataType.UserTags, null) + { + this.searchAction = searchAction; + } + + protected override void AddMetadata(string[]? tags, LinkFlowContainer loaded) + { + if (tags == null) + return; + + for (int i = 0; i <= tags.Length - 1; i++) + { + string tag = tags[i]; + + if (searchAction != null) + loaded.AddLink(tag, () => searchAction(tag)); + else + loaded.AddLink(tag, LinkAction.SearchBeatmapSet, $@"tag=""""{tag}"""""); + + if (i != tags.Length - 1) + loaded.AddText(" "); + } + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/MetadataType.cs b/osu.Game/Overlays/BeatmapSet/MetadataType.cs index c92cecc17e..dba6a63679 100644 --- a/osu.Game/Overlays/BeatmapSet/MetadataType.cs +++ b/osu.Game/Overlays/BeatmapSet/MetadataType.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; @@ -8,8 +9,11 @@ namespace osu.Game.Overlays.BeatmapSet { public enum MetadataType { - [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoTags))] - Tags, + [Description("User Tags")] // TODO: use translated string after osu-resources update + UserTags, + + [Description("Mapper Tags")] // TODO: use translated string after osu-resources update + MapperTags, [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))] Source, diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 8de21129d3..255e30038b 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -47,7 +47,10 @@ namespace osu.Game.Overlays Spacing = new Vector2(0, 20), Children = new Drawable[] { - info = new Info(), + info = new Info + { + Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap } + }, new ScoresContainer { Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap } @@ -60,11 +63,7 @@ namespace osu.Game.Overlays info.BeatmapSet.BindTo(beatmapSet); comments.BeatmapSet.BindTo(beatmapSet); - Header.HeaderContent.Picker.Beatmap.ValueChanged += b => - { - info.BeatmapInfo = b.NewValue; - ScrollFlow.ScrollToStart(); - }; + Header.HeaderContent.Picker.Beatmap.ValueChanged += b => ScrollFlow.ScrollToStart(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 2bb60716ff..6a6a4cddf3 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Select { description = new MetadataSectionDescription(query => songSelect?.Search(query)), source = new MetadataSectionSource(query => songSelect?.Search(query)), - tags = new MetadataSectionTags(query => songSelect?.Search(query)), + tags = new MetadataSectionMapperTags(query => songSelect?.Search(query)), }, }, }, From 0eb89f94f8390c366e035aeaf86353881b75d341 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 27 Mar 2025 23:11:15 -0400 Subject: [PATCH 042/281] Fix chevron alignment in dropdown menu items --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 5a1fbaa3a4..af335efdc4 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -252,6 +252,7 @@ namespace osu.Game.Graphics.UserInterface Size = new Vector2(8), Alpha = 0, X = chevron_offset, + Y = 1, Margin = new MarginPadding { Left = 3, Right = 3 }, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, From 9b9cfbc9c2fda9cbc62e9357b9384eaa3220b811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Mar 2025 13:24:26 +0100 Subject: [PATCH 043/281] Redesign vote buttons to better work with hierarchical tags --- .../Visual/Ranking/TestSceneUserTagControl.cs | 8 ++-- osu.Game/Screens/Ranking/UserTagControl.cs | 42 +++++++++++++++---- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index d622df8d76..f05aa46054 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -36,10 +36,10 @@ namespace osu.Game.Tests.Visual.Ranking { Tags = [ - new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", }, - new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", }, - new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", }, - new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", }, + new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", }, + new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", }, + new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", }, + new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", }, ] }), 500); return true; diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index ae4a918ae5..f516a80cfb 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -249,6 +249,7 @@ namespace osu.Game.Screens.Ranking private Box mainBackground = null!; private Box voteBackground = null!; + private OsuSpriteText tagCategoryText = null!; private OsuSpriteText tagNameText = null!; private OsuSpriteText voteCountText = null!; private LoadingSpinner spinner = null!; @@ -276,6 +277,8 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { + string[] tagParts = UserTag.Name.Split('/'); + Anchor = Anchor.Centre; Origin = Anchor.Centre; CornerRadius = 8; @@ -297,21 +300,42 @@ namespace osu.Game.Screens.Ranking { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Left = 6, Right = 3, Vertical = 3, }, - Spacing = new Vector2(5), Children = new Drawable[] { - tagNameText = new OsuSpriteText + tagCategoryText = new OsuSpriteText { - Text = UserTag.Name, + Alpha = tagParts.Length > 1 ? 0.6f : 0, + Text = tagParts[0], Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Horizontal = 6 } + }, + new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, + Blending = BlendingParameters.Additive, + }, + tagNameText = new OsuSpriteText + { + Text = tagParts[^1], + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Horizontal = 6 } + }, + } }, new Container { AutoSizeAxes = Axes.Both, - CornerRadius = 5, - Masking = true, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Children = new Drawable[] @@ -353,7 +377,7 @@ namespace osu.Game.Screens.Ranking { if (v.NewValue) { - voteBackground.FadeColour(colours.Lime3, transition_duration, Easing.OutQuint); + voteBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint); voteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); } else @@ -366,13 +390,15 @@ namespace osu.Game.Screens.Ranking { if (c.NewValue) { - mainBackground.FadeColour(colours.Lime1, transition_duration, Easing.OutQuint); + mainBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint); + tagCategoryText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); tagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); FadeEdgeEffectTo(0.5f, transition_duration, Easing.OutQuint); } else { mainBackground.FadeColour(colours.Gray4, transition_duration, Easing.OutQuint); + tagCategoryText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); tagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint); } From 133a1bc1fada7b2ca93cbcd9c4e9e7385a7907c8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Mar 2025 21:26:14 +0900 Subject: [PATCH 044/281] Add test for changing item in list Brought forward from https://github.com/ppy/osu/pull/32250. --- .../TestSceneMultiplayerQueueList.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 7283e3a1fe..d7659351bb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -49,13 +49,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create playlist", () => { - Child = playlist = new MultiplayerQueueList(room) + Child = playlist = new MultiplayerQueueList { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(500, 300), + Size = new Vector2(500, 300) }; + playlist.Items.ReplaceRange(0, playlist.Items.Count, MultiplayerClient.ClientAPIRoom!.Playlist); + MultiplayerClient.ClientAPIRoom!.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(Room.Playlist)) @@ -132,6 +134,18 @@ namespace osu.Game.Tests.Visual.Multiplayer assertDeleteButtonVisibility(1, false); } + [Test] + public void TestChangeExistingItem() + { + AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem + { + ID = playlist.Items[0].ID, + BeatmapID = 1337 + }).WaitSafely()); + + AddUntilStep("first playlist item has new beatmap", () => playlist.Items[0].Beatmap.OnlineID, () => Is.EqualTo(1337)); + } + private void addPlaylistItem(Func userId) { long itemId = -1; From 67eb691c8310620b629211626ec5425de9b2538f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Mar 2025 21:49:32 +0900 Subject: [PATCH 045/281] Fix room ids not being incremented in tests Tests will [preserve](https://github.com/ppy/osu/blob/f4c96ecb54ff7650e617597faa583f1f40ce323b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs#L270) the incoming `RoomId` value if non-null. But since now everything goes through `MultiplayerClient.CreateRoom(MultiplayerRoom)` which internally creates a representative `Room` to return in requests, this would previously return a `Room` with `RoomId == 0` and not the expected `null` value. The only consumer of this outside of tests is [server-spectator](https://github.com/ppy/osu-server-spectator/blob/c1f33672cc2245614bdcfeb3109801b20a89c709/osu.Server.Spectator/Services/SharedInterop.cs#L147), but only in the context of creating rooms at which point RoomId is irrelevant. --- osu.Game/Online/Rooms/Room.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index e965f9c187..b93917eff6 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -348,7 +348,7 @@ namespace osu.Game.Online.Rooms public Room(MultiplayerRoom room) { - RoomID = room.RoomID; + RoomID = room.RoomID > 0 ? room.RoomID : null; Name = room.Settings.Name; Password = room.Settings.Password; Type = room.Settings.MatchType; From b93e21c6667acfbf51e88ac2a0f62da58daf20a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Mar 2025 21:51:30 +0900 Subject: [PATCH 046/281] Refactor `MultiplayerPlaylist` to not have a selected item bindable --- .../TestSceneMultiplayerPlaylist.cs | 6 +- .../Match/Playlist/MultiplayerPlaylist.cs | 61 ++++++++----------- .../Match/Playlist/MultiplayerQueueList.cs | 35 +---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 5 +- 4 files changed, 31 insertions(+), 76 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 1affa08813..1bde02762e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Platform; @@ -53,13 +52,12 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create list", () => { - Child = list = new MultiplayerPlaylist(room) + Child = list = new MultiplayerPlaylist { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.4f, 0.8f), - SelectedItem = new Bindable() + Size = new Vector2(0.4f, 0.8f) }; }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 9feee0ae41..8f59db467d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -19,12 +20,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { public readonly Bindable DisplayMode = new Bindable(); - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - /// /// Invoked when an item requests to be edited. /// @@ -33,18 +28,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist [Resolved] private MultiplayerClient client { get; set; } = null!; - private readonly Room room; - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private MultiplayerPlaylistTabControl playlistTabControl = null!; private MultiplayerQueueList queueList = null!; private MultiplayerHistoryList historyList = null!; private bool firstPopulation = true; - public MultiplayerPlaylist(Room room) - { - this.room = room; - } - [BackgroundDependencyLoader] private void load() { @@ -65,17 +53,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist Masking = true, Children = new Drawable[] { - queueList = new MultiplayerQueueList(room) + queueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = selectedItem }, RequestEdit = item => RequestEdit?.Invoke(item) }, historyList = new MultiplayerHistoryList { RelativeSizeAxes = Axes.Both, Alpha = 0, - SelectedItem = { BindTarget = selectedItem } } } } @@ -89,10 +75,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist base.LoadComplete(); DisplayMode.BindValueChanged(onDisplayModeChanged, true); + client.ItemAdded += playlistItemAdded; client.ItemRemoved += playlistItemRemoved; client.ItemChanged += playlistItemChanged; client.RoomUpdated += onRoomUpdated; + updateState(); } @@ -121,28 +109,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist firstPopulation = false; } + + // As a small optimisation, only the ID is required to match the selected item. + PlaylistItem? selectedItem = client.Room == null ? null : new PlaylistItem(new APIBeatmap()) { ID = client.Room.Settings.PlaylistItemId }; + queueList.SelectedItem.Value = selectedItem; + historyList.SelectedItem.Value = selectedItem; } - private void playlistItemAdded(MultiplayerPlaylistItem item) => Schedule(() => addItemToLists(item)); + private void playlistItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => addItemToLists(item)); - private void playlistItemRemoved(long item) => Schedule(() => removeItemFromLists(item)); + private void playlistItemRemoved(long item) => Scheduler.Add(() => removeItemFromLists(item)); - private void playlistItemChanged(MultiplayerPlaylistItem item) => Schedule(() => + private void playlistItemChanged(MultiplayerPlaylistItem item) => Scheduler.Add(() => { if (client.Room == null) return; - var newApiItem = new PlaylistItem(item); - var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID); + var existingItem = queueList.Items.SingleOrDefault(i => i.ID == item.ID); // Test if the only change between the two playlist items is the order. - if (existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem)) + if (existingItem != null && existingItem.With(playlistOrder: item.PlaylistOrder).Equals(new PlaylistItem(item))) { - // Set the new playlist order directly without refreshing the DrawablePlaylistItem. - existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder; - - // The following isn't really required, but is here for safety and explicitness. - // MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation. + // Set the new order directly and refresh the flow layout as an optimisation to avoid refreshing the items' visual state. + existingItem.PlaylistOrder = item.PlaylistOrder; queueList.Invalidate(); } else @@ -154,22 +143,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private void addItemToLists(MultiplayerPlaylistItem item) { - var apiItem = client.Room?.Playlist.SingleOrDefault(i => i.ID == item.ID); - - // Item could have been removed from the playlist while the local player was in gameplay. - if (apiItem == null) + if (client.Room == null) return; if (item.Expired) - historyList.Items.Add(new PlaylistItem(apiItem)); + historyList.Items.Add(new PlaylistItem(item)); else - queueList.Items.Add(new PlaylistItem(apiItem)); + queueList.Items.Add(new PlaylistItem(item)); } - private void removeItemFromLists(long item) + private void removeItemFromLists(long itemId) { - queueList.Items.RemoveAll(i => i.ID == item); - historyList.Items.RemoveAll(i => i.ID == item); + if (client.Room == null) + return; + + queueList.Items.RemoveAll(i => i.ID == itemId); + historyList.Items.RemoveAll(i => i.ID == itemId); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 04bb9b69e6..dc6a713908 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; @@ -20,50 +19,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist /// public partial class MultiplayerQueueList : DrawableRoomPlaylist { - private readonly Room room; - - private QueueFillFlowContainer flow = null!; - - public MultiplayerQueueList(Room room) + public MultiplayerQueueList() { - this.room = room; ShowItemOwners = true; } - protected override void LoadComplete() - { - base.LoadComplete(); - - room.PropertyChanged += onRoomPropertyChanged; - updateRoomPlaylist(); - } - - private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(Room.Playlist)) - updateRoomPlaylist(); - } - - private void updateRoomPlaylist() - => flow.InvalidateLayout(); - - protected override FillFlowContainer> CreateListFillFlowContainer() => flow = new QueueFillFlowContainer + protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer { Spacing = new Vector2(0, 2) }; protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item); - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - room.PropertyChanged -= onRoomPropertyChanged; - } - private partial class QueueFillFlowContainer : FillFlowContainer> { - public new void InvalidateLayout() => base.InvalidateLayout(); - public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.PlaylistOrder); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 6c6932f479..08a469fa03 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -145,11 +145,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer null, new Drawable[] { - new MultiplayerPlaylist(Room) + new MultiplayerPlaylist { RelativeSizeAxes = Axes.Both, - RequestEdit = OpenSongSelection, - SelectedItem = SelectedItem + RequestEdit = OpenSongSelection } }, new[] From 1a0432ce3a17aaf8da1bf84e0526a0aca8fa7145 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Mar 2025 22:01:23 +0900 Subject: [PATCH 047/281] Re-enable ignored playlist test --- .../TestSceneMultiplayerPlaylist.cs | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 1bde02762e..c6a203c77a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -31,7 +31,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; - private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -46,8 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("create room", () => room = CreateDefaultRoom()); - AddStep("join room", () => JoinRoom(room)); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); WaitForJoined(); AddStep("create list", () => @@ -156,37 +154,36 @@ namespace osu.Game.Tests.Visual.Multiplayer assertQueueTabCount(0); } - [Ignore("Expired items are initially removed from the room.")] [Test] public void TestJoinRoomWithMixedItemsAddedInCorrectLists() { AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); - AddStep("join room with items", () => + AddStep("join room with expired items", () => { - API.Queue(new CreateRoomRequest(new Room - { - Name = "test name", - Playlist = - [ - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID - }, - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID, - Expired = true - } - ] - })); + Room room = CreateDefaultRoom(); + room.Playlist = + [ + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID + }, + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID, + Expired = true + } + ]; + + JoinRoom(room); }); - AddUntilStep("wait for room join", () => RoomJoined); + WaitForJoined(); - assertItemInQueueListStep(1, 0); - assertItemInHistoryListStep(2, 0); + // IDs are offset by 1 because we've joined two rooms in this test. + assertItemInQueueListStep(2, 0); + assertItemInHistoryListStep(3, 0); } [Test] From 1d44d5e10088f6d94ac45d3c0ca98df5ef40601f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 28 Mar 2025 14:05:35 +0100 Subject: [PATCH 048/281] Move the extra tag selector out of popover --- .../Ranking/TestSceneStatisticsPanel.cs | 12 +- osu.Game/Screens/Ranking/UserTagControl.cs | 200 ++++++++---------- 2 files changed, 93 insertions(+), 119 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 814c0519a3..ea80f2c5b2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -214,14 +214,10 @@ namespace osu.Game.Tests.Visual.Ranking { Tags = [ - new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", }, - new APITag - { - Id = 2, Name = "alt", - Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", - }, - new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", }, - new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", }, + new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", }, + new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", }, + new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", }, + new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", }, ] }), 500); return true; diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index f516a80cfb..3a71aaadd6 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -8,15 +8,11 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -26,12 +22,9 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays; using osuTK; using osuTK.Input; @@ -66,35 +59,53 @@ namespace osu.Game.Screens.Ranking private void load(SessionStatics sessionStatics) { AutoSizeAxes = Axes.Y; + InternalChildren = new Drawable[] { - new FillFlowContainer + new GridContainer { - Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(8), - Children = new Drawable[] + Padding = new MarginPadding(10), + ColumnDimensions = + [ + new Dimension(GridSizeMode.Absolute, 300), + new Dimension() + ], + RowDimensions = [new Dimension(GridSizeMode.AutoSize, minSize: 250)], + Content = new[] { - tagFlow = new FillFlowContainer + new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, - LayoutDuration = 300, - LayoutEasing = Easing.OutQuint, - Spacing = new Vector2(4), - }, - new AddTagsButton - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - OnTagSelected = onExtraTagSelected, - AvailableTags = { BindTarget = extraTags }, - }, - }, + new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(8), + Children = new Drawable[] + { + tagFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + LayoutDuration = 300, + LayoutEasing = Easing.OutQuint, + Spacing = new Vector2(4), + }, + }, + }, + new TagList + { + RelativeSizeAxes = Axes.Both, + AvailableTags = { BindTarget = extraTags }, + OnSelected = onExtraTagSelected, + } + } + } }, - loadingLayer = new LoadingLayer + loadingLayer = new LoadingLayer(dimBackground: true) { RelativeSizeAxes = Axes.Both, State = { Value = Visibility.Visible } @@ -239,6 +250,8 @@ namespace osu.Game.Screens.Ranking } } + protected override bool OnClick(ClickEvent e) => true; + private partial class DrawableUserTag : OsuAnimatedButton { public readonly UserTag UserTag; @@ -279,8 +292,8 @@ namespace osu.Game.Screens.Ranking { string[] tagParts = UserTag.Name.Split('/'); - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; CornerRadius = 8; Masking = true; EdgeEffect = new EdgeEffectParameters @@ -452,35 +465,7 @@ namespace osu.Game.Screens.Ranking } } - private partial class AddTagsButton : GrayButton, IHasPopover - { - public BindableList AvailableTags { get; } = new BindableList(); - - public Action? OnTagSelected { get; set; } - - public AddTagsButton() - : base(FontAwesome.Solid.Plus) - { - Size = new Vector2(30); - - Action = this.ShowPopover; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AvailableTags.BindCollectionChanged((_, _) => Enabled.Value = AvailableTags.Count > 0, true); - } - - public Popover GetPopover() => new AddTagsPopover - { - AvailableTags = { BindTarget = AvailableTags }, - OnSelected = OnTagSelected, - }; - } - - private partial class AddTagsPopover : OsuPopover + private partial class TagList : CompositeDrawable { private SearchTextBox searchBox = null!; private SearchContainer searchContainer = null!; @@ -490,33 +475,44 @@ namespace osu.Game.Screens.Ranking public Action? OnSelected { get; set; } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { - Child = new OsuScrollContainer + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] { - Width = 250, - Height = 250, - ScrollbarOverlapsContent = false, - Children = new Drawable[] + new Box { - searchBox = new SearchTextBox - { - HoldFocus = true, - RelativeSizeAxes = Axes.X, - }, - searchContainer = new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Right = 5, Top = 50, }, - Spacing = new Vector2(10), - ChildrenEnumerable = AvailableTags.Select(tag => new DrawableAddableTag(tag) - { - Action = () => select(tag) - }) - } + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + searchBox = new SearchTextBox + { + HoldFocus = true, + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10) { Top = 45, }, + ScrollbarOverlapsContent = false, + Child = searchContainer = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + } + } + }, + } }; } @@ -524,27 +520,15 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); - } - - public override bool OnPressed(KeyBindingPressEvent e) - { - if (base.OnPressed(e)) - return true; - - if (e.Repeat) - return false; - - if (State.Value == Visibility.Hidden) - return false; - - if (e.Action == GlobalAction.Select) + AvailableTags.BindCollectionChanged((_, _) => { - attemptSelect(); - return true; - } - - return false; + searchContainer.Clear(); + searchContainer.ChildrenEnumerable = AvailableTags.Select(tag => new DrawableAddableTag(tag) + { + Action = () => OnSelected?.Invoke(tag) + }); + }, true); + searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } protected override bool OnKeyDown(KeyDownEvent e) @@ -563,13 +547,7 @@ namespace osu.Game.Screens.Ranking var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); if (visibleItems.Length == 1) - select(visibleItems.Single().Tag); - } - - private void select(UserTag tag) - { - OnSelected?.Invoke(tag); - this.HidePopover(); + OnSelected?.Invoke(visibleItems.Single().Tag); } private partial class DrawableAddableTag : OsuAnimatedButton, IFilterable @@ -586,14 +564,14 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider? colourProvider) + private void load(OsuColour colours) { Content.AddRange(new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider?.Background3 ?? colours.GreySeaFoamDark, + Colour = colours.Gray6, Depth = float.MaxValue, }, new FillFlowContainer From 36e1077f99a6545b1540e306b2c4e3279a8202a5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 28 Mar 2025 22:07:59 +0900 Subject: [PATCH 049/281] Add missing disposal unbinds This is an issue on `master` and have likely been causing some leaks. --- .../Match/Playlist/MultiplayerPlaylist.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 8f59db467d..d44cb1dde8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; @@ -160,5 +161,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist queueList.Items.RemoveAll(i => i.ID == itemId); historyList.Items.RemoveAll(i => i.ID == itemId); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.ItemAdded -= playlistItemAdded; + client.ItemRemoved -= playlistItemRemoved; + client.ItemChanged -= playlistItemChanged; + client.RoomUpdated -= onRoomUpdated; + } + } } } From fa06643bb6c0aacde659640ae0a65c68ab9b0c61 Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:44:14 +0100 Subject: [PATCH 050/281] Use median for statistic display --- osu.Game/Screens/Ranking/Statistics/AverageHitError.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs index fb7107cc88..29df085c62 100644 --- a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs +++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs @@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Ranking.Statistics { /// - /// Displays the unstable rate statistic for a given play. + /// Displays the average hit error statistic for a given play. /// public partial class AverageHitError : SimpleStatisticItem { /// /// Creates and computes an statistic. /// - /// Sequence of s to calculate the unstable rate based on. + /// Sequence of s to calculate the average hit error based on. public AverageHitError(IEnumerable hitEvents) : base("Average Hit Error") { - Value = hitEvents.CalculateAverageHitError(); + Value = hitEvents.CalculateMedianHitError(); } protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}"; From b3c578e5455c572e34e2def301ba657182747149 Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:45:39 +0100 Subject: [PATCH 051/281] Remove mean hit error calculation --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 01d800a351..39fc8b357b 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -54,23 +54,6 @@ namespace osu.Game.Rulesets.Scoring return result; } - /// - /// Calculates the average hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. - /// - /// - /// A non-null value if unstable rate could be calculated, - /// and if unstable rate cannot be calculated due to being empty. - /// - public static double? CalculateAverageHitError(this IEnumerable hitEvents) - { - double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); - - if (timeOffsets.Length == 0) - return null; - - return timeOffsets.Average(); - } - /// /// Calculates the median hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. /// From 403b24ef9bb32d95dc3f701ee9fce8b217d16848 Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Sat, 29 Mar 2025 20:43:59 +0100 Subject: [PATCH 052/281] Adapt `TestNotEnoughTimedHitEvents` with new minimum hit amount --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index aa99b22701..92a10628ff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -50,21 +50,17 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("Set short reference score", () => { + // 50 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows List hitEvents = [ - // 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), ]; + for (int i = 0; i < 49; i++) + { + hitEvents.Add(new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null)); + } + foreach (var ev in hitEvents) ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); From acffe31e41bad582d7b55563c28d35c8a50d795e Mon Sep 17 00:00:00 2001 From: iamnotcoding Date: Sun, 30 Mar 2025 17:47:33 +0900 Subject: [PATCH 053/281] Made it able to include or exclude multiple key mods by comma separated values --- .../ManiaFilterCriteria.cs | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 8c6efbc72d..de5b7c4d2f 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -17,20 +17,50 @@ namespace osu.Game.Rulesets.Mania { public class ManiaFilterCriteria : IRulesetFilterCriteria { - private FilterCriteria.OptionalRange keys; - + private FilterCriteria.OptionalRange included_key_range; + private HashSet included_keys = new HashSet(); + private HashSet excluded_keys = new HashSet(); public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { - return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods)); + bool result = (!included_key_range.HasFilter) && (included_keys.Count == 0); + int key_index = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); + + result |= (included_key_range.HasFilter && included_key_range.IsInRange(key_index)) || included_keys.Contains(key_index); + result &= !excluded_keys.Contains(key_index); + + return result; } - public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) + public bool TryParseCustomKeywordCriteria(string key, Operator op, string str_values) { switch (key) { case "key": case "keys": - return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value); + if (op == Operator.Equal) + { + foreach (string str_value in str_values.Split(',')) + { + if (int.TryParse(str_value, out int value)) + { + if (value > 0) + { + included_keys.Add(value); + } + else + { + excluded_keys.Add(-value); + } + } + } + + return true; + } + else + { + // In this case, the str_values is a string of a single value + return FilterQueryParser.TryUpdateCriteriaRange(ref included_key_range, op, str_values); + } } return false; @@ -38,7 +68,7 @@ namespace osu.Game.Rulesets.Mania public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { - if (keys.HasFilter) + if (included_key_range.HasFilter || included_keys.Count != 0 || excluded_keys.Count != 0) { // Interpreting as the Mod type is required for equality comparison. HashSet oldSet = mods.OldValue.OfType().AsEnumerable().ToHashSet(); From 117e91bfabc343f467b98fea80ea9d9cff017ff2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 31 Mar 2025 16:21:49 +0900 Subject: [PATCH 054/281] Remove local optimisation, use `CurrentPlaylistItem` --- .../Multiplayer/Match/Playlist/MultiplayerPlaylist.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index d44cb1dde8..fba3acc32a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -8,7 +8,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -111,10 +110,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist firstPopulation = false; } - // As a small optimisation, only the ID is required to match the selected item. - PlaylistItem? selectedItem = client.Room == null ? null : new PlaylistItem(new APIBeatmap()) { ID = client.Room.Settings.PlaylistItemId }; - queueList.SelectedItem.Value = selectedItem; - historyList.SelectedItem.Value = selectedItem; + PlaylistItem? currentItem = client.Room == null ? null : new PlaylistItem(client.Room.CurrentPlaylistItem); + queueList.SelectedItem.Value = currentItem; + historyList.SelectedItem.Value = currentItem; } private void playlistItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => addItemToLists(item)); From 74d234a7debd6be49a8d6eba8045368ab65d1b07 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 31 Mar 2025 16:32:32 +0900 Subject: [PATCH 055/281] Replace RoomID change with test-local fixes --- .../Visual/DailyChallenge/TestSceneDailyChallenge.cs | 8 ++------ .../DailyChallenge/TestSceneDailyChallengeIntro.cs | 11 +++++------ osu.Game/Online/Rooms/Room.cs | 2 +- .../Visual/OnlinePlay/TestRoomRequestsHandler.cs | 2 +- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index c974a852f3..185ebc1d39 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -43,7 +43,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge { var room = new Room { - RoomID = 1234, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge { var room = new Room { - RoomID = 1234, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -99,7 +97,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge { var room = new Room { - RoomID = 1234, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -114,7 +111,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge }; AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); - AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value }); Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); @@ -128,7 +125,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge { var room = new Room { - RoomID = 1234, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -143,7 +139,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge }; AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); - AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value }); Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs index d6665e24a4..97b957df43 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.DailyChallenge [Test] public void TestDailyChallenge() { - startChallenge(1234); + startChallenge(); AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room))); } [Test] public void TestPlayIntroOnceFlag() { - startChallenge(1234); + startChallenge(); AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); - startChallenge(1235); + startChallenge(); AddAssert("intro played flag reset", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.False); @@ -62,13 +62,12 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddUntilStep("intro played flag set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.True); } - private void startChallenge(int roomId) + private void startChallenge() { AddStep("add room", () => { API.Perform(new CreateRoomRequest(room = new Room { - RoomID = roomId, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -83,7 +82,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge Category = RoomCategory.DailyChallenge })); }); - AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId })); + AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value })); } } } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index b93917eff6..e965f9c187 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -348,7 +348,7 @@ namespace osu.Game.Online.Rooms public Room(MultiplayerRoom room) { - RoomID = room.RoomID > 0 ? room.RoomID : null; + RoomID = room.RoomID; Name = room.Settings.Name; Password = room.Settings.Password; Type = room.Settings.MatchType; diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 46c1251d42..08f61f3ddc 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// The room host. public void AddServerSideRoom(Room room, APIUser host) { - room.RoomID ??= currentRoomId++; + room.RoomID = currentRoomId++; room.Host = host; for (int i = 0; i < room.Playlist.Count; i++) From dadb014a69d971bfc04392d67c7719958560be7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 09:49:49 +0200 Subject: [PATCH 056/281] Adjust transform --- osu.Game/Screens/Ranking/UserTagControl.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 3a71aaadd6..a8f6f2fcb1 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -604,6 +604,15 @@ namespace osu.Game.Screens.Ranking public bool MatchingFilter { set => Alpha = value ? 1 : 0; } public bool FilteringActive { set { } } + + protected override bool OnMouseDown(MouseDownEvent e) + { + bool result = base.OnMouseDown(e); + // slightly dodgy way of overriding the amount of scale-on-click (the default is way too much in this case) + ClearTransforms(targetMember: nameof(Scale)); + Content.ScaleTo(0.95f, 2000, Easing.OutQuint); + return result; + } } } } From acc5790b740ffa4f805f8891375aa29c2332cffa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Mar 2025 17:15:25 +0900 Subject: [PATCH 057/281] Fix slider repeat arrow fade not matching expectations --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index bc48f34828..e0fd7953f1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this .FadeOut() .Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0) - .FadeIn(HitObject.RepeatIndex == 0 ? HitObject.TimeFadeIn : animDuration); + .FadeIn(150); } protected override void UpdateHitStateTransforms(ArmedState state) From 624948f0826ad5ae2f1548295f12778508a8dd3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 10:39:34 +0200 Subject: [PATCH 058/281] Add groupings to all tags list --- .../Visual/Ranking/TestSceneUserTagControl.cs | 14 ++-- .../Statistics/StatisticItemContainer.cs | 39 +---------- .../Ranking/Statistics/StatisticItemHeader.cs | 68 +++++++++++++++++++ osu.Game/Screens/Ranking/UserTag.cs | 10 ++- osu.Game/Screens/Ranking/UserTagControl.cs | 59 ++++++++++++---- 5 files changed, 131 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticItemHeader.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index f05aa46054..cfedd89b12 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Graphics; -using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; @@ -36,6 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking { Tags = [ + new APITag { Id = 0, Name = "uncategorised tag", Description = "This probably isn't real but could be and should be handled.", }, new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", }, new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", }, new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", }, @@ -69,15 +69,11 @@ namespace osu.Game.Tests.Visual.Ranking }); AddStep("create control", () => { - Child = new PopoverContainer + Child = new UserTagControl(Beatmap.Value.BeatmapInfo) { - RelativeSizeAxes = Axes.Both, - Child = new UserTagControl(Beatmap.Value.BeatmapInfo) - { - Width = 500, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Width = 700, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }; }); } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs index 6e18ae1fe4..8caf8d66b5 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItemContainer.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osuTK; namespace osu.Game.Screens.Ranking.Statistics { @@ -53,7 +50,9 @@ namespace osu.Game.Screens.Ranking.Statistics Padding = new MarginPadding(5), Children = new[] { - createHeader(item), + LocalisableString.IsNullOrEmpty(item.Name) + ? Empty() + : new StatisticItemHeader { Text = item.Name }, new Container { RelativeSizeAxes = Axes.X, @@ -66,37 +65,5 @@ namespace osu.Game.Screens.Ranking.Statistics } }; } - - private static Drawable createHeader(StatisticItem item) - { - if (LocalisableString.IsNullOrEmpty(item.Name)) - return Empty(); - - return new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - Height = 20, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), - Children = new Drawable[] - { - new Circle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Height = 9, - Width = 4, - Colour = Color4Extensions.FromHex("#00FFAA") - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = item.Name, - Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold), - } - } - }; - } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItemHeader.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItemHeader.cs new file mode 100644 index 0000000000..6b496e10dd --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItemHeader.cs @@ -0,0 +1,68 @@ +// 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.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public partial class StatisticItemHeader : CompositeDrawable, IHasText + { + public LocalisableString Text + { + get => text; + set + { + if (text == value) return; + + text = value; + if (IsLoaded) + spriteText.Text = value; + } + } + + private LocalisableString text; + private OsuSpriteText spriteText = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + Height = 20, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 9, + Width = 4, + Colour = Color4Extensions.FromHex("#00FFAA") + }, + spriteText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = text, + Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold), + } + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/UserTag.cs b/osu.Game/Screens/Ranking/UserTag.cs index d44e531330..983f585931 100644 --- a/osu.Game/Screens/Ranking/UserTag.cs +++ b/osu.Game/Screens/Ranking/UserTag.cs @@ -9,7 +9,9 @@ namespace osu.Game.Screens.Ranking public record UserTag { public long Id { get; } - public string Name { get; } + public string FullName { get; } + public string? GroupName { get; } + public string DisplayName { get; } public string Description { get; } public BindableInt VoteCount { get; } = new BindableInt(); @@ -18,8 +20,12 @@ namespace osu.Game.Screens.Ranking public UserTag(APITag tag) { Id = tag.Id; - Name = tag.Name; + FullName = tag.Name; Description = tag.Description; + + string[] splitName = FullName.Split('/'); + GroupName = splitName.Length > 1 ? splitName[0] : null; + DisplayName = splitName[^1]; } } } diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index a8f6f2fcb1..95a72f2142 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -25,6 +25,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Ranking.Statistics; using osuTK; using osuTK.Input; @@ -290,8 +291,6 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { - string[] tagParts = UserTag.Name.Split('/'); - Anchor = Anchor.CentreLeft; Origin = Anchor.CentreLeft; CornerRadius = 8; @@ -317,8 +316,8 @@ namespace osu.Game.Screens.Ranking { tagCategoryText = new OsuSpriteText { - Alpha = tagParts.Length > 1 ? 0.6f : 0, - Text = tagParts[0], + Alpha = UserTag.GroupName != null ? 0.6f : 0, + Text = UserTag.GroupName ?? default(LocalisableString), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Margin = new MarginPadding { Horizontal = 6 } @@ -339,7 +338,7 @@ namespace osu.Game.Screens.Ranking }, tagNameText = new OsuSpriteText { - Text = tagParts[^1], + Text = UserTag.DisplayName, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Margin = new MarginPadding { Horizontal = 6 } @@ -497,6 +496,7 @@ namespace osu.Game.Screens.Ranking HoldFocus = true, RelativeSizeAxes = Axes.X, Depth = float.MinValue, + Y = -2, // hacky compensation for masking issues }, new OsuScrollContainer { @@ -523,14 +523,26 @@ namespace osu.Game.Screens.Ranking AvailableTags.BindCollectionChanged((_, _) => { searchContainer.Clear(); - searchContainer.ChildrenEnumerable = AvailableTags.Select(tag => new DrawableAddableTag(tag) - { - Action = () => OnSelected?.Invoke(tag) - }); + searchContainer.ChildrenEnumerable = createItems(AvailableTags); }, true); searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } + private IEnumerable createItems(IEnumerable tags) + { + var grouped = tags.GroupBy(tag => tag.GroupName).OrderBy(group => group.Key); + + foreach (var group in grouped) + { + var drawableGroup = new GroupFlow(group.Key); + + foreach (var tag in group.OrderBy(t => t.FullName)) + drawableGroup.Add(new DrawableAddableTag(tag) { Action = () => OnSelected?.Invoke(tag) }); + + yield return drawableGroup; + } + } + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Key == Key.Enter) @@ -550,6 +562,30 @@ namespace osu.Game.Screens.Ranking OnSelected?.Invoke(visibleItems.Single().Tag); } + private partial class GroupFlow : FillFlowContainer, IFilterable + { + public IEnumerable FilterTerms { get; } + + public bool MatchingFilter + { + set => Alpha = value ? 1 : 0; + } + + public bool FilteringActive { set { } } + + public GroupFlow(string? name) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Spacing = new Vector2(5); + + Add(new StatisticItemHeader { Text = name ?? "uncategorised" }); + + FilterTerms = name == null ? [] : [name]; + } + } + private partial class DrawableAddableTag : OsuAnimatedButton, IFilterable { public readonly UserTag Tag; @@ -560,7 +596,6 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Anchor = Origin = Anchor.Centre; } [BackgroundDependencyLoader] @@ -587,7 +622,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = Tag.Name, + Text = Tag.DisplayName, }, new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 14)) { @@ -600,7 +635,7 @@ namespace osu.Game.Screens.Ranking }); } - public IEnumerable FilterTerms => [Tag.Name, Tag.Description]; + public IEnumerable FilterTerms => [Tag.FullName, Tag.Description]; public bool MatchingFilter { set => Alpha = value ? 1 : 0; } public bool FilteringActive { set { } } From 80879319bb7eae34b15887dfe6d3b3c6686456df Mon Sep 17 00:00:00 2001 From: iamnotcoding Date: Mon, 31 Mar 2025 17:48:56 +0900 Subject: [PATCH 059/281] Changed code quality issues --- .../ManiaFilterCriteria.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index de5b7c4d2f..78386a21f5 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -17,21 +17,22 @@ namespace osu.Game.Rulesets.Mania { public class ManiaFilterCriteria : IRulesetFilterCriteria { - private FilterCriteria.OptionalRange included_key_range; - private HashSet included_keys = new HashSet(); - private HashSet excluded_keys = new HashSet(); + private FilterCriteria.OptionalRange includedKeyCountRange; + private readonly HashSet includedKeyCounts = new HashSet(); + private readonly HashSet excludedKeyCounts = new HashSet(); + public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { - bool result = (!included_key_range.HasFilter) && (included_keys.Count == 0); - int key_index = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); + bool result = !includedKeyCountRange.HasFilter && includedKeyCounts.Count == 0; + int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); - result |= (included_key_range.HasFilter && included_key_range.IsInRange(key_index)) || included_keys.Contains(key_index); - result &= !excluded_keys.Contains(key_index); + result |= (includedKeyCountRange.HasFilter && includedKeyCountRange.IsInRange(keyCount)) || includedKeyCounts.Contains(keyCount); + result &= !excludedKeyCounts.Contains(keyCount); return result; } - public bool TryParseCustomKeywordCriteria(string key, Operator op, string str_values) + public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues) { switch (key) { @@ -39,17 +40,17 @@ namespace osu.Game.Rulesets.Mania case "keys": if (op == Operator.Equal) { - foreach (string str_value in str_values.Split(',')) + foreach (string strValue in strValues.Split(',')) { - if (int.TryParse(str_value, out int value)) + if (int.TryParse(strValue, out int value)) { if (value > 0) { - included_keys.Add(value); + includedKeyCounts.Add(value); } else { - excluded_keys.Add(-value); + excludedKeyCounts.Add(-value); } } } @@ -58,8 +59,8 @@ namespace osu.Game.Rulesets.Mania } else { - // In this case, the str_values is a string of a single value - return FilterQueryParser.TryUpdateCriteriaRange(ref included_key_range, op, str_values); + // In this case, the strValues is a string of a single value + return FilterQueryParser.TryUpdateCriteriaRange(ref includedKeyCountRange, op, strValues); } } @@ -68,7 +69,7 @@ namespace osu.Game.Rulesets.Mania public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { - if (included_key_range.HasFilter || included_keys.Count != 0 || excluded_keys.Count != 0) + if (includedKeyCountRange.HasFilter || includedKeyCounts.Count != 0 || excludedKeyCounts.Count != 0) { // Interpreting as the Mod type is required for equality comparison. HashSet oldSet = mods.OldValue.OfType().AsEnumerable().ToHashSet(); From 8e562c1ded68c43e9e86ff21bd416818b2d2a782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 11:03:18 +0200 Subject: [PATCH 060/281] Adjust flow direction --- osu.Game/Screens/Ranking/UserTagControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 95a72f2142..1bc0062c10 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Full, + Direction = FillDirection.Vertical, LayoutDuration = 300, LayoutEasing = Easing.OutQuint, Spacing = new Vector2(4), From e6f3c5351cafcd5868e6b2e832e0e8ad7623e7c2 Mon Sep 17 00:00:00 2001 From: iamnotcoding Date: Mon, 31 Mar 2025 18:32:51 +0900 Subject: [PATCH 061/281] Made it use only one has variable and deleted key exclude function for now --- .../ManiaFilterCriteria.cs | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 78386a21f5..612fe57c84 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -17,19 +17,13 @@ namespace osu.Game.Rulesets.Mania { public class ManiaFilterCriteria : IRulesetFilterCriteria { - private FilterCriteria.OptionalRange includedKeyCountRange; - private readonly HashSet includedKeyCounts = new HashSet(); - private readonly HashSet excludedKeyCounts = new HashSet(); + private readonly HashSet includedKeyCounts = Enumerable.Range(1, 20).ToHashSet(); public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { - bool result = !includedKeyCountRange.HasFilter && includedKeyCounts.Count == 0; int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); - result |= (includedKeyCountRange.HasFilter && includedKeyCountRange.IsInRange(keyCount)) || includedKeyCounts.Contains(keyCount); - result &= !excludedKeyCounts.Contains(keyCount); - - return result; + return includedKeyCounts.Contains(keyCount); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues) @@ -40,6 +34,8 @@ namespace osu.Game.Rulesets.Mania case "keys": if (op == Operator.Equal) { + includedKeyCounts.Clear(); + foreach (string strValue in strValues.Split(',')) { if (int.TryParse(strValue, out int value)) @@ -50,26 +46,53 @@ namespace osu.Game.Rulesets.Mania } else { - excludedKeyCounts.Add(-value); + return false; } } + else + { + return false; + } } - - return true; } else { - // In this case, the strValues is a string of a single value - return FilterQueryParser.TryUpdateCriteriaRange(ref includedKeyCountRange, op, strValues); + if (!int.TryParse(strValues, out int value)) + { + return false; + } + + if (value <= 0) + { + return false; + } + + switch (op) + { + case Operator.Less: + includedKeyCounts.RemoveWhere(k => k >= value); + break; + case Operator.LessOrEqual: + includedKeyCounts.RemoveWhere(k => k > value); + break; + case Operator.Greater: + includedKeyCounts.RemoveWhere(k => k <= value); + break; + case Operator.GreaterOrEqual: + includedKeyCounts.RemoveWhere(k => k < value); + break; + } } + + break; } - return false; + return true; } public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { - if (includedKeyCountRange.HasFilter || includedKeyCounts.Count != 0 || excludedKeyCounts.Count != 0) + if (includedKeyCounts.Count > 0) { // Interpreting as the Mod type is required for equality comparison. HashSet oldSet = mods.OldValue.OfType().AsEnumerable().ToHashSet(); From 1e315a3af6cd8267e8dc53b360f0d6e31d75457a Mon Sep 17 00:00:00 2001 From: iamnotcoding Date: Mon, 31 Mar 2025 18:34:51 +0900 Subject: [PATCH 062/281] Added NotEqual(!=) operator --- osu.Game/Screens/Select/Filter/Operator.cs | 1 + osu.Game/Screens/Select/FilterQueryParser.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Screens/Select/Filter/Operator.cs b/osu.Game/Screens/Select/Filter/Operator.cs index 706daf631f..e9d9af548e 100644 --- a/osu.Game/Screens/Select/Filter/Operator.cs +++ b/osu.Game/Screens/Select/Filter/Operator.cs @@ -11,6 +11,7 @@ namespace osu.Game.Screens.Select.Filter Less, LessOrEqual, Equal, + NotEqual, GreaterOrEqual, Greater } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 78f3bab114..cde8850d9f 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -129,6 +129,9 @@ namespace osu.Game.Screens.Select case ":": return Operator.Equal; + case "!=": + return Operator.NotEqual; + case "<": return Operator.Less; From e2bdb70daac8ff1d875ddc8a5f84a2ad55334659 Mon Sep 17 00:00:00 2001 From: iamnotcoding Date: Mon, 31 Mar 2025 18:39:02 +0900 Subject: [PATCH 063/281] Added the "!:" option for NotEqual operator --- .../ManiaFilterCriteria.cs | 23 +++++++++++++++++++ osu.Game/Screens/Select/FilterQueryParser.cs | 1 + 2 files changed, 24 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 612fe57c84..15f790d8c3 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -55,6 +55,27 @@ namespace osu.Game.Rulesets.Mania } } } + else if (op == Operator.NotEqual) + { + foreach (string strValue in strValues.Split(',')) + { + if (int.TryParse(strValue, out int value)) + { + if (value > 0) + { + includedKeyCounts.Remove(value); + } + else + { + return false; + } + } + else + { + return false; + } + } + } else { if (!int.TryParse(strValues, out int value)) @@ -81,6 +102,8 @@ namespace osu.Game.Rulesets.Mania case Operator.GreaterOrEqual: includedKeyCounts.RemoveWhere(k => k < value); break; + default: + return false; } } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index cde8850d9f..229b328e49 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -130,6 +130,7 @@ namespace osu.Game.Screens.Select return Operator.Equal; case "!=": + case "!:": return Operator.NotEqual; case "<": From a9ba61595933c20a647747ec7a0adca3bd7a92cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 11:41:46 +0200 Subject: [PATCH 064/281] Always show all tags in full list & allow voting from both views --- osu.Game/Screens/Ranking/UserTagControl.cs | 225 ++++++++++++--------- 1 file changed, 124 insertions(+), 101 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 1bc0062c10..1bffbc699e 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -8,11 +8,14 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -43,11 +46,14 @@ namespace osu.Game.Screens.Ranking private LoadingLayer loadingLayer = null!; private BindableList displayedTags { get; } = new BindableList(); - private BindableList extraTags { get; } = new BindableList(); - private Bindable allTags = null!; + private Bindable apiTags = null!; + private BindableDictionary allTagsById { get; } = new BindableDictionary(); + private readonly Bindable apiBeatmap = new Bindable(); + private APIRequest? requestInFlight; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -100,8 +106,8 @@ namespace osu.Game.Screens.Ranking new TagList { RelativeSizeAxes = Axes.Both, - AvailableTags = { BindTarget = extraTags }, - OnSelected = onExtraTagSelected, + AvailableTags = { BindTarget = allTagsById }, + OnSelected = toggleVote, } } } @@ -113,12 +119,12 @@ namespace osu.Game.Screens.Ranking }, }; - allTags = sessionStatics.GetBindable(Static.AllBeatmapTags); + apiTags = sessionStatics.GetBindable(Static.AllBeatmapTags); - if (allTags.Value == null) + if (apiTags.Value == null) { var listTagsRequest = new ListTagsRequest(); - listTagsRequest.Success += tags => allTags.Value = tags.Tags.ToArray(); + listTagsRequest.Success += tags => apiTags.Value = tags.Tags.ToArray(); api.Queue(listTagsRequest); } @@ -127,28 +133,11 @@ namespace osu.Game.Screens.Ranking api.Queue(getBeatmapSetRequest); } - private void onExtraTagSelected(UserTag tag) - { - loadingLayer.Show(); - extraTags.Remove(tag); - - var req = new AddBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id); - req.Success += () => - { - tag.Voted.Value = true; - tag.VoteCount.Value += 1; - displayedTags.Add(tag); - loadingLayer.Hide(); - }; - req.Failure += _ => extraTags.Add(tag); - api.Queue(req); - } - protected override void LoadComplete() { base.LoadComplete(); - allTags.BindValueChanged(_ => updateTags()); + apiTags.BindValueChanged(_ => updateTags()); apiBeatmap.BindValueChanged(_ => updateTags()); updateTags(); @@ -157,25 +146,26 @@ namespace osu.Game.Screens.Ranking private void updateTags() { - if (allTags.Value == null || apiBeatmap.Value?.TopTags == null) + if (apiTags.Value == null || apiBeatmap.Value == null) return; - var allTagsById = allTags.Value.ToDictionary(t => t.Id); - var ownTagIds = apiBeatmap.Value.OwnTagIds?.ToHashSet() ?? new HashSet(); + allTagsById.Clear(); + allTagsById.AddRange(apiTags.Value.Select(t => new KeyValuePair(t.Id, new UserTag(t)))); - foreach (var topTag in apiBeatmap.Value.TopTags) + foreach (var topTag in apiBeatmap.Value.TopTags ?? []) { - if (allTagsById.Remove(topTag.TagId, out var tag)) + if (allTagsById.TryGetValue(topTag.TagId, out var tag)) { - displayedTags.Add(new UserTag(tag) - { - VoteCount = { Value = topTag.VoteCount }, - Voted = { Value = ownTagIds.Contains(tag.Id) } - }); + tag.VoteCount.Value = topTag.VoteCount; + displayedTags.Add(tag); } } - extraTags.AddRange(allTagsById.Select(t => new UserTag(t.Value))); + foreach (long ownTagId in apiBeatmap.Value.OwnTagIds ?? []) + { + if (allTagsById.TryGetValue(ownTagId, out var tag)) + tag.Voted.Value = true; + } loadingLayer.Hide(); } @@ -191,7 +181,7 @@ namespace osu.Game.Screens.Ranking for (int i = 0; i < e.NewItems!.Count; i++) { var tag = (UserTag)e.NewItems[i]!; - var drawableTag = new DrawableUserTag(tag); + var drawableTag = new DrawableUserTag(tag) { OnSelected = toggleVote }; tagFlow.Insert(tagFlow.Count, drawableTag); tag.VoteCount.BindValueChanged(voteCountChanged, true); layout.Invalidate(); @@ -220,15 +210,59 @@ namespace osu.Game.Screens.Ranking } } + private void toggleVote(UserTag tag) + { + if (requestInFlight != null) + return; + + loadingLayer.Show(); + + APIRequest request; + + switch (tag.Voted.Value) + { + case true: + var removeReq = new RemoveBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id); + removeReq.Success += () => + { + tag.VoteCount.Value -= 1; + tag.Voted.Value = false; + }; + request = removeReq; + break; + + case false: + var addReq = new AddBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id); + addReq.Success += () => + { + tag.VoteCount.Value += 1; + tag.Voted.Value = true; + if (!displayedTags.Contains(tag)) + displayedTags.Add(tag); + }; + request = addReq; + break; + } + + request.Success += () => + { + loadingLayer.Hide(); + requestInFlight = null; + }; + request.Failure += _ => + { + loadingLayer.Hide(); + requestInFlight = null; + }; + api.Queue(requestInFlight = request); + } + private void voteCountChanged(ValueChangedEvent _) { var tagsWithNoVotes = displayedTags.Where(t => t.VoteCount.Value == 0).ToArray(); foreach (var tag in tagsWithNoVotes) - { displayedTags.Remove(tag); - extraTags.Add(tag); - } layout.Invalidate(); } @@ -257,6 +291,8 @@ namespace osu.Game.Screens.Ranking { public readonly UserTag UserTag; + public Action? OnSelected { get; set; } + private readonly Bindable voteCount = new Bindable(); private readonly BindableBool voted = new BindableBool(); private readonly Bindable confirmed = new BindableBool(); @@ -266,19 +302,10 @@ namespace osu.Game.Screens.Ranking private OsuSpriteText tagCategoryText = null!; private OsuSpriteText tagNameText = null!; private OsuSpriteText voteCountText = null!; - private LoadingSpinner spinner = null!; [Resolved] private OsuColour colours { get; set; } = null!; - [Resolved] - private Bindable beatmap { get; set; } = null!; - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - private APIRequest? requestInFlight; - public DrawableUserTag(UserTag userTag) { UserTag = userTag; @@ -360,11 +387,6 @@ namespace osu.Game.Screens.Ranking { Margin = new MarginPadding { Horizontal = 6, Vertical = 3, }, }, - spinner = new LoadingSpinner(withBox: true) - { - Alpha = 0, - Size = new Vector2(18), - } } } } @@ -417,50 +439,7 @@ namespace osu.Game.Screens.Ranking }, true); FinishTransforms(true); - Action = () => - { - if (requestInFlight != null) - return; - - spinner.Show(); - - APIRequest request; - - switch (voted.Value) - { - case true: - var removeReq = new RemoveBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id); - removeReq.Success += () => - { - voteCount.Value -= 1; - voted.Value = false; - }; - request = removeReq; - break; - - case false: - var addReq = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, UserTag.Id); - addReq.Success += () => - { - voteCount.Value += 1; - voted.Value = true; - }; - request = addReq; - break; - } - - request.Success += () => - { - spinner.Hide(); - requestInFlight = null; - }; - request.Failure += _ => - { - spinner.Hide(); - requestInFlight = null; - }; - api.Queue(requestInFlight = request); - }; + Action = () => OnSelected?.Invoke(UserTag); } } @@ -469,7 +448,7 @@ namespace osu.Game.Screens.Ranking private SearchTextBox searchBox = null!; private SearchContainer searchContainer = null!; - public BindableList AvailableTags { get; } = new BindableList(); + public BindableDictionary AvailableTags { get; } = new BindableDictionary(); public Action? OnSelected { get; set; } @@ -501,12 +480,13 @@ namespace osu.Game.Screens.Ranking new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10) { Top = 45, }, + Padding = new MarginPadding { Top = 40, }, ScrollbarOverlapsContent = false, Child = searchContainer = new SearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10, Bottom = 10 }, Direction = FillDirection.Vertical, Spacing = new Vector2(10), } @@ -523,7 +503,7 @@ namespace osu.Game.Screens.Ranking AvailableTags.BindCollectionChanged((_, _) => { searchContainer.Clear(); - searchContainer.ChildrenEnumerable = createItems(AvailableTags); + searchContainer.ChildrenEnumerable = createItems(AvailableTags.Values); }, true); searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } @@ -590,6 +570,10 @@ namespace osu.Game.Screens.Ranking { public readonly UserTag Tag; + private Container votedIndicator = null!; + + private readonly Bindable voted = new Bindable(); + public DrawableAddableTag(UserTag tag) { Tag = tag; @@ -609,10 +593,36 @@ namespace osu.Game.Screens.Ranking Colour = colours.Gray6, Depth = float.MaxValue, }, + votedIndicator = new Container + { + RelativeSizeAxes = Axes.Both, + Width = 0.1f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Depth = float.MaxValue, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(colours.Lime1.Opacity(0), colours.Lime1.Opacity(0.4f)), + }, + new SpriteIcon + { + Size = new Vector2(16), + Icon = FontAwesome.Solid.ThumbsUp, + Colour = colours.Lime1, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 5 }, + } + } + }, new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Width = 0.9f, Direction = FillDirection.Vertical, Spacing = new Vector2(2), Padding = new MarginPadding(5), @@ -633,6 +643,8 @@ namespace osu.Game.Screens.Ranking } } }); + + voted.BindTo(Tag.Voted); } public IEnumerable FilterTerms => [Tag.FullName, Tag.Description]; @@ -640,6 +652,17 @@ namespace osu.Game.Screens.Ranking public bool MatchingFilter { set => Alpha = value ? 1 : 0; } public bool FilteringActive { set { } } + protected override void LoadComplete() + { + base.LoadComplete(); + + voted.BindValueChanged(_ => + { + votedIndicator.FadeTo(voted.Value ? 1 : 0, 250, Easing.OutQuint); + }, true); + FinishTransforms(true); + } + protected override bool OnMouseDown(MouseDownEvent e) { bool result = base.OnMouseDown(e); From 9d75bb43a30984caf474063ca01b944024916e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 11:50:46 +0200 Subject: [PATCH 065/281] Final sizing adjustments --- osu.Game/Screens/Ranking/UserTagControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 1bffbc699e..506b34fe85 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Ranking Padding = new MarginPadding(10), ColumnDimensions = [ - new Dimension(GridSizeMode.Absolute, 300), + new Dimension(GridSizeMode.Absolute, 350), new Dimension() ], RowDimensions = [new Dimension(GridSizeMode.AutoSize, minSize: 250)], @@ -614,7 +614,7 @@ namespace osu.Game.Screens.Ranking Colour = colours.Lime1, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Margin = new MarginPadding { Right = 5 }, + Margin = new MarginPadding { Right = 10 }, } } }, From 8ff63e158c3e549573714b5677e32e85bbd9dd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 11:54:22 +0200 Subject: [PATCH 066/281] Async load panels to avoid update thread hitching --- osu.Game/Screens/Ranking/UserTagControl.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 506b34fe85..cd5d2486fd 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -452,6 +453,8 @@ namespace osu.Game.Screens.Ranking public Action? OnSelected { get; set; } + private CancellationTokenSource? loadCancellationTokenSource; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -502,8 +505,14 @@ namespace osu.Game.Screens.Ranking AvailableTags.BindCollectionChanged((_, _) => { - searchContainer.Clear(); - searchContainer.ChildrenEnumerable = createItems(AvailableTags.Values); + loadCancellationTokenSource?.Cancel(); + loadCancellationTokenSource = new CancellationTokenSource(); + + LoadComponentsAsync(createItems(AvailableTags.Values), loaded => + { + searchContainer.Clear(); + searchContainer.AddRange(loaded); + }, loadCancellationTokenSource.Token); }, true); searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); } From 6cc48eb976de65d71ed2b4f89936adff7122ea3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 12:13:50 +0200 Subject: [PATCH 067/281] Fix enter-to-select-tag interaction --- osu.Game/Screens/Ranking/UserTagControl.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index cd5d2486fd..727e2b5f4a 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -17,8 +17,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Extensions; @@ -26,12 +28,12 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Ranking.Statistics; using osuTK; -using osuTK.Input; namespace osu.Game.Screens.Ranking { @@ -288,6 +290,10 @@ namespace osu.Game.Screens.Ranking protected override bool OnClick(ClickEvent e) => true; + public void OnReleased(KeyBindingReleaseEvent e) + { + } + private partial class DrawableUserTag : OsuAnimatedButton { public readonly UserTag UserTag; @@ -444,7 +450,7 @@ namespace osu.Game.Screens.Ranking } } - private partial class TagList : CompositeDrawable + private partial class TagList : CompositeDrawable, IKeyBindingHandler { private SearchTextBox searchBox = null!; private SearchContainer searchContainer = null!; @@ -532,20 +538,24 @@ namespace osu.Game.Screens.Ranking } } - protected override bool OnKeyDown(KeyDownEvent e) + public bool OnPressed(KeyBindingPressEvent e) { - if (e.Key == Key.Enter) + if (e.Action == GlobalAction.Select && !e.Repeat) { attemptSelect(); return true; } - return base.OnKeyDown(e); + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { } private void attemptSelect() { - var visibleItems = searchContainer.OfType().Where(d => d.IsPresent).ToArray(); + var visibleItems = searchContainer.ChildrenOfType().Where(d => d.IsPresent).ToArray(); if (visibleItems.Length == 1) OnSelected?.Invoke(visibleItems.Single().Tag); From 545575eab34f135cce49f95b631b47bc40528f60 Mon Sep 17 00:00:00 2001 From: iamnotcoding Date: Mon, 31 Mar 2025 19:15:41 +0900 Subject: [PATCH 068/281] Changed the Regex expression to make it able to detect the NotEqual (!= or !:) operator --- osu.Game/Screens/Select/FilterQueryParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 229b328e49..1094d88730 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select public static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))", + @"\b(?\w+)(?(!?(:|=)|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) From 9761a64d52d06d4b871a1c9a81695d0e1fdc4fd5 Mon Sep 17 00:00:00 2001 From: iamnotcoding Date: Mon, 31 Mar 2025 20:26:31 +0900 Subject: [PATCH 069/281] Made key with equal operator intersect with other filters and removed unnecessary value range check --- .../ManiaFilterCriteria.cs | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 15f790d8c3..5e066ca5c9 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -34,13 +34,14 @@ namespace osu.Game.Rulesets.Mania case "keys": if (op == Operator.Equal) { - includedKeyCounts.Clear(); - - foreach (string strValue in strValues.Split(',')) + // If the filter is empty + if (includedKeyCounts.Count == 20) { - if (int.TryParse(strValue, out int value)) + includedKeyCounts.Clear(); + + foreach (string strValue in strValues.Split(',')) { - if (value > 0) + if (int.TryParse(strValue, out int value)) { includedKeyCounts.Add(value); } @@ -49,10 +50,24 @@ namespace osu.Game.Rulesets.Mania return false; } } - else + } + else + { + HashSet tmp = new HashSet(); + + foreach (string strValue in strValues.Split(',')) { - return false; + if (int.TryParse(strValue, out int value)) + { + tmp.Add(value); + } + else + { + return false; + } } + + includedKeyCounts.IntersectWith(tmp); } } else if (op == Operator.NotEqual) @@ -61,14 +76,7 @@ namespace osu.Game.Rulesets.Mania { if (int.TryParse(strValue, out int value)) { - if (value > 0) - { - includedKeyCounts.Remove(value); - } - else - { - return false; - } + includedKeyCounts.Remove(value); } else { @@ -83,11 +91,6 @@ namespace osu.Game.Rulesets.Mania return false; } - if (value <= 0) - { - return false; - } - switch (op) { case Operator.Less: @@ -108,6 +111,9 @@ namespace osu.Game.Rulesets.Mania } break; + + default: + return false; } return true; From 8e3d05cbb01ad934bad1bedde0d0e93740885c44 Mon Sep 17 00:00:00 2001 From: iamnotcoding Date: Mon, 31 Mar 2025 20:27:22 +0900 Subject: [PATCH 070/281] Fixed the filter empty condition --- osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 5e066ca5c9..61ffa91626 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Mania public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { - if (includedKeyCounts.Count > 0) + if (includedKeyCounts.Count != 20) { // Interpreting as the Mod type is required for equality comparison. HashSet oldSet = mods.OldValue.OfType().AsEnumerable().ToHashSet(); From 79375eccb7e0d3717c5ca10ff4688f2d9a015f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 14:23:56 +0200 Subject: [PATCH 071/281] Add failing test case Co-authored-by: Rodrigo Correia --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 91188f5bac..97889eea4d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; @@ -458,6 +459,62 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); } + [Test] + public void TestAnchorRadioButtonBehavior() + { + ISerialisableDrawable? selectedComponent = null; + + AddStep("Select first component", () => + { + var blueprint = skinEditor.ChildrenOfType().First(); + skinEditor.SelectedComponents.Clear(); + skinEditor.SelectedComponents.Add(blueprint.Item); + selectedComponent = blueprint.Item; + }); + + AddStep("Right-click to open context menu", () => + { + if (selectedComponent != null) + InputManager.MoveMouseTo(((Drawable)selectedComponent).ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Right); + }); + + AddStep("Click on Anchor menu", () => + { + InputManager.MoveMouseTo(getMenuItemByText("Anchor")); + InputManager.Click(MouseButton.Left); + }); + + AddStep("Right-click TopLeft anchor", () => + { + InputManager.MoveMouseTo(getMenuItemByText("TopLeft")); + InputManager.Click(MouseButton.Right); + }); + + AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True); + + AddStep("Right-click Centre anchor", () => + { + InputManager.MoveMouseTo(getMenuItemByText("Centre")); + InputManager.Click(MouseButton.Right); + }); + + AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True); + AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False); + + AddStep("Right-click Closest anchor", () => + { + InputManager.MoveMouseTo(getMenuItemByText("Closest")); + InputManager.Click(MouseButton.Right); + }); + + AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True); + AddAssert("Centre item unchecked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False); + + Menu.DrawableMenuItem getMenuItemByText(string text) + => this.ChildrenOfType().First(m => m.Item.Text.ToString() == text); + } + private Skin importSkinFromArchives(string filename) { var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely(); From 736f2af1e04f7e4c588483874f9e55035a7f077d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Mar 2025 14:24:42 +0200 Subject: [PATCH 072/281] Fix skin editor anchor/origin context menu ternary states not updating properly - Closes https://github.com/ppy/osu/issues/31797 - Supersedes / closes https://github.com/ppy/osu/pull/32611 --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 9 +- .../SkinEditor/SkinSelectionHandler.cs | 107 ++++++++++++------ 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 534abd1ab3..c1c64cac1f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -760,11 +760,7 @@ namespace osu.Game.Overlays.SkinEditor #region Delegation of IEditorChangeHandler - public event Action? OnStateChange - { - add => throw new NotImplementedException(); - remove => throw new NotImplementedException(); - } + public event Action? OnStateChange; private IEditorChangeHandler? beginChangeHandler; @@ -773,6 +769,9 @@ namespace osu.Game.Overlays.SkinEditor // Change handler may change between begin and end, which can cause unbalanced operations. // Let's track the one that was used when beginning the change so we can call EndChange on it specifically. (beginChangeHandler = changeHandler)?.BeginChange(); + + if (beginChangeHandler != null) + beginChangeHandler.OnStateChange += OnStateChange; } public void EndChange() => beginChangeHandler?.EndChange(); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index bc878b9214..23270a1097 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -22,7 +22,10 @@ namespace osu.Game.Overlays.SkinEditor { public partial class SkinSelectionHandler : SelectionHandler { - private OsuMenuItem originMenu = null!; + private OsuMenuItem? originMenu; + + private TernaryStateRadioMenuItem? closestAnchor; + private AnchorMenuItem[]? fixedAnchors; [Resolved] private SkinEditor skinEditor { get; set; } = null!; @@ -44,6 +47,38 @@ namespace osu.Game.Overlays.SkinEditor return scaleHandler; } + protected override void LoadComplete() + { + base.LoadComplete(); + + if (ChangeHandler != null) + ChangeHandler.OnStateChange += updateTernaryStates; + SelectedItems.BindCollectionChanged((_, _) => updateTernaryStates()); + } + + private void updateTernaryStates() + { + var usingClosestAnchor = GetStateFromSelection(SelectedBlueprints, c => !c.Item.UsesFixedAnchor); + + if (closestAnchor != null) + closestAnchor.State.Value = usingClosestAnchor; + + if (fixedAnchors != null) + { + foreach (var fixedAnchor in fixedAnchors) + fixedAnchor.State.Value = GetStateFromSelection(SelectedBlueprints, c => c.Item.UsesFixedAnchor && ((Drawable)c.Item).Anchor == fixedAnchor.Anchor); + } + + if (originMenu != null) + { + foreach (var origin in originMenu.Items.OfType()) + { + origin.State.Value = GetStateFromSelection(SelectedBlueprints, c => ((Drawable)c.Item).Origin == origin.Anchor); + origin.Action.Disabled = usingClosestAnchor == TernaryState.True; + } + } + } + public override bool HandleFlip(Direction direction, bool flipOverOrigin) { var selectionQuad = getSelectionQuad(); @@ -102,27 +137,19 @@ namespace osu.Game.Overlays.SkinEditor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { - var closestItem = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors()) - { - State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) } - }; + closestAnchor = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors()); + fixedAnchors = createAnchorItems(applyFixedAnchors).ToArray(); yield return new OsuMenuItem(SkinEditorStrings.Anchor) { - Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors) - .Prepend(closestItem) - .ToArray() + Items = fixedAnchors.Prepend(closestAnchor).ToArray() }; yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin); - closestItem.State.BindValueChanged(s => - { - // For UX simplicity, origin should only be user-editable when "closest" anchor mode is disabled. - originMenu.Items = s.NewValue == TernaryState.True - ? Array.Empty() - : createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray(); - }, true); + var usingClosestAnchor = GetStateFromSelection(SelectedBlueprints, c => !c.Item.UsesFixedAnchor); + + originMenu.Items = createAnchorItems(applyOrigins).ToArray(); yield return new OsuMenuItemSpacer(); @@ -163,27 +190,37 @@ namespace osu.Game.Overlays.SkinEditor foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) + updateTernaryStates(); + } + + private IEnumerable createAnchorItems(Action applyFunction) + { + var displayableAnchors = new[] { - var displayableAnchors = new[] - { - Anchor.TopLeft, - Anchor.TopCentre, - Anchor.TopRight, - Anchor.CentreLeft, - Anchor.Centre, - Anchor.CentreRight, - Anchor.BottomLeft, - Anchor.BottomCentre, - Anchor.BottomRight, - }; - return displayableAnchors.Select(a => - { - return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a)) - { - State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) } - }; - }); + Anchor.TopLeft, + Anchor.TopCentre, + Anchor.TopRight, + Anchor.CentreLeft, + Anchor.Centre, + Anchor.CentreRight, + Anchor.BottomLeft, + Anchor.BottomCentre, + Anchor.BottomRight, + }; + return displayableAnchors.Select(a => + { + return new AnchorMenuItem(a, _ => applyFunction(a)); + }); + } + + private partial class AnchorMenuItem : TernaryStateRadioMenuItem + { + public readonly Anchor Anchor; + + public AnchorMenuItem(Anchor anchor, Action applyFunction) + : base(anchor.ToString(), MenuItemType.Standard, _ => applyFunction(anchor)) + { + Anchor = anchor; } } From 005b204f0a6be4cd83ba379e413c9f17092f7e92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Mar 2025 23:43:04 +0900 Subject: [PATCH 073/281] Remove unused local --- osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 23270a1097..838b5ff2f0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -147,8 +147,6 @@ namespace osu.Game.Overlays.SkinEditor yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin); - var usingClosestAnchor = GetStateFromSelection(SelectedBlueprints, c => !c.Item.UsesFixedAnchor); - originMenu.Items = createAnchorItems(applyOrigins).ToArray(); yield return new OsuMenuItemSpacer(); From 8de96201566175e3cc65dd9db3d61e66d2bf4285 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Apr 2025 14:41:17 +0900 Subject: [PATCH 074/281] Isolate operation of multiplayer mod overlay --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 26 +---- .../Match/MultiplayerUserModSelectOverlay.cs | 108 ++++++++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 36 ------ 3 files changed, 111 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 57e8aff151..c73a36617d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.ComponentModel; using System.Linq; using osu.Framework.Allocation; @@ -27,6 +26,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Utils; using Container = osu.Framework.Graphics.Containers.Container; @@ -62,11 +62,6 @@ namespace osu.Game.Screens.OnlinePlay.Match private Sample? sampleStart; - /// - /// Any mods applied by/to the local user. - /// - protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); - [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -245,12 +240,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } }; - LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay - { - SelectedItem = { BindTarget = SelectedItem }, - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - }); + LoadComponent(UserModsSelectOverlay = new MultiplayerUserModSelectOverlay()); } protected override void LoadComplete() @@ -258,7 +248,6 @@ namespace osu.Game.Screens.OnlinePlay.Match base.LoadComplete(); SelectedItem.BindValueChanged(_ => updateSpecifics()); - UserMods.BindValueChanged(_ => updateSpecifics()); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -441,11 +430,6 @@ namespace osu.Game.Screens.OnlinePlay.Match ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - // Remove any user mods that are no longer allowed. - Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); - if (!newUserMods.SequenceEqual(UserMods.Value)) - UserMods.Value = newUserMods; - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); @@ -456,15 +440,11 @@ namespace osu.Game.Screens.OnlinePlay.Match Ruleset.Value = GetGameplayRuleset(); if (allowedMods.Length > 0) - { UserModsSection.Show(); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } else { UserModsSection.Hide(); UserModsSelectOverlay.Hide(); - UserModsSelectOverlay.IsValidMod = _ => false; } if (item.Freestyle) @@ -488,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleSection.Hide(); } - protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + protected virtual APIMod[] GetGameplayMods() => SelectedItem.Value!.RequiredMods; protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs new file mode 100644 index 0000000000..e5c447f038 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Threading; +using osu.Game.Configuration; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Utils; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class MultiplayerUserModSelectOverlay : RoomModSelectOverlay + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private ModSettingChangeTracker? modSettingChangeTracker; + private ScheduledDelegate? debouncedModSettingsUpdate; + + protected override void LoadComplete() + { + base.LoadComplete(); + + IsValidMod = _ => false; + + client.RoomUpdated += onRoomUpdated; + + SelectedItem.BindValueChanged(_ => updateSpecifics()); + SelectedMods.BindValueChanged(_ => updateSpecifics()); + SelectedMods.BindValueChanged(onSelectedModsChanged); + } + + private void onRoomUpdated() + { + if (client.Room == null) + return; + + SelectedItem.Value = new PlaylistItem(client.Room.CurrentPlaylistItem); + } + + private void onSelectedModsChanged(ValueChangedEvent> mods) + { + modSettingChangeTracker?.Dispose(); + + if (client.Room == null) + return; + + client.ChangeUserMods(mods.NewValue).FireAndForget(); + + modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + modSettingChangeTracker.SettingChanged += _ => + { + // Debounce changes to mod settings so as to not thrash the network. + debouncedModSettingsUpdate?.Cancel(); + debouncedModSettingsUpdate = Scheduler.AddDelayed(() => + { + if (client.Room == null) + return; + + client.ChangeUserMods(SelectedMods.Value).FireAndForget(); + }, 500); + }; + } + + private void updateSpecifics() + { + if (client.Room == null || client.LocalUser == null) + return; + + MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem; + Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance(); + Mod[] allowedMods = currentItem.Freestyle + ? ruleset.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, client.Room.Settings.MatchType)).ToArray() + : currentItem.AllowedMods.Select(m => m.ToMod(ruleset)).ToArray(); + + // Update the mod panels to reflect the ones which are valid for selection. + IsValidMod = allowedMods.Length > 0 + ? m => allowedMods.Any(a => a.GetType() == m.GetType()) + : _ => false; + + // Remove any mods that are no longer allowed. + Mod[] newUserMods = SelectedMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + if (!newUserMods.SequenceEqual(SelectedMods.Value)) + SelectedMods.Value = newUserMods; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + + modSettingChangeTracker?.Dispose(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 08a469fa03..0cc033907f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.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.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -11,9 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Framework.Threading; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; using osu.Game.Online.API; @@ -23,7 +20,6 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -64,7 +60,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); - UserMods.BindValueChanged(onUserModsChanged); client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; @@ -306,35 +301,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void PartRoom() => client.LeaveRoom(); - private ModSettingChangeTracker? modSettingChangeTracker; - private ScheduledDelegate? debouncedModSettingsUpdate; - - private void onUserModsChanged(ValueChangedEvent> mods) - { - modSettingChangeTracker?.Dispose(); - - if (client.Room == null) - return; - - client.ChangeUserMods(mods.NewValue).FireAndForget(); - - modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); - modSettingChangeTracker.SettingChanged += onModSettingsChanged; - } - - private void onModSettingsChanged(Mod mod) - { - // Debounce changes to mod settings so as to not thrash the network. - debouncedModSettingsUpdate?.Cancel(); - debouncedModSettingsUpdate = Scheduler.AddDelayed(() => - { - if (client.Room == null) - return; - - client.ChangeUserMods(UserMods.Value).FireAndForget(); - }, 500); - } - private void updateBeatmapAvailability(ValueChangedEvent availability) { if (client.Room == null) @@ -462,8 +428,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.RoomUpdated -= onRoomUpdated; client.LoadRequested -= onLoadRequested; } - - modSettingChangeTracker?.Dispose(); } public partial class AddItemButton : PurpleRoundedButton From 72efbbad2dec4565fa46003056a60d26b1d82c9f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Apr 2025 16:45:25 +0900 Subject: [PATCH 075/281] Remove inheritance on `RoomModSelectOverlay` --- .../Match/MultiplayerUserModSelectOverlay.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index e5c447f038..1ddcccc02c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -10,14 +10,15 @@ using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class MultiplayerUserModSelectOverlay : RoomModSelectOverlay + public class MultiplayerUserModSelectOverlay : UserModSelectOverlay { [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -28,26 +29,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private ModSettingChangeTracker? modSettingChangeTracker; private ScheduledDelegate? debouncedModSettingsUpdate; + public MultiplayerUserModSelectOverlay() + : base(OverlayColourScheme.Plum) + { + } + protected override void LoadComplete() { base.LoadComplete(); - IsValidMod = _ => false; - client.RoomUpdated += onRoomUpdated; - - SelectedItem.BindValueChanged(_ => updateSpecifics()); - SelectedMods.BindValueChanged(_ => updateSpecifics()); SelectedMods.BindValueChanged(onSelectedModsChanged); + + updateValidMods(); } - private void onRoomUpdated() - { - if (client.Room == null) - return; - - SelectedItem.Value = new PlaylistItem(client.Room.CurrentPlaylistItem); - } + private void onRoomUpdated() => Scheduler.AddOnce(updateValidMods); private void onSelectedModsChanged(ValueChangedEvent> mods) { @@ -73,7 +70,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }; } - private void updateSpecifics() + private void updateValidMods() { if (client.Room == null || client.LocalUser == null) return; @@ -95,6 +92,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match SelectedMods.Value = newUserMods; } + protected override IReadOnlyList ComputeActiveMods() + { + if (client.Room == null || client.LocalUser == null) + return []; + + MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem; + Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance(); + return currentItem.RequiredMods.Select(m => m.ToMod(ruleset)).Concat(base.ComputeActiveMods()).ToArray(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From fbc8469fc402c57e4fb146e13d162fab8030c119 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Apr 2025 17:10:47 +0900 Subject: [PATCH 076/281] Partial class --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index 1ddcccc02c..075b664028 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -18,7 +18,7 @@ using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class MultiplayerUserModSelectOverlay : UserModSelectOverlay + public partial class MultiplayerUserModSelectOverlay : UserModSelectOverlay { [Resolved] private MultiplayerClient client { get; set; } = null!; From 452f36d77ab337f5150f84ac735a365000124def Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 1 Apr 2025 17:55:19 +0900 Subject: [PATCH 077/281] Fix active mods not updated --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index 075b664028..c66e1a906c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -90,6 +90,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Mod[] newUserMods = SelectedMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); if (!newUserMods.SequenceEqual(SelectedMods.Value)) SelectedMods.Value = newUserMods; + + ActiveMods.Value = ComputeActiveMods(); } protected override IReadOnlyList ComputeActiveMods() From dcf35ff1042d1c912aef5c5d889b9de0acab198d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Apr 2025 13:47:10 +0200 Subject: [PATCH 078/281] Unify voted displays --- osu.Game/Screens/Ranking/UserTagControl.cs | 33 +++++++++++----------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 727e2b5f4a..da3059aaf4 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -9,10 +9,8 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; @@ -589,7 +587,8 @@ namespace osu.Game.Screens.Ranking { public readonly UserTag Tag; - private Container votedIndicator = null!; + private Box votedBackground = null!; + private SpriteIcon votedIcon = null!; private readonly Bindable voted = new Bindable(); @@ -601,8 +600,11 @@ namespace osu.Game.Screens.Ranking AutoSizeAxes = Axes.Y; } + [Resolved] + private OsuColour colours { get; set; } = null!; + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Content.AddRange(new Drawable[] { @@ -612,28 +614,25 @@ namespace osu.Game.Screens.Ranking Colour = colours.Gray6, Depth = float.MaxValue, }, - votedIndicator = new Container + new Container { - RelativeSizeAxes = Axes.Both, - Width = 0.1f, + RelativeSizeAxes = Axes.Y, + Width = 30, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Depth = float.MaxValue, Children = new Drawable[] { - new Box + votedBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(colours.Lime1.Opacity(0), colours.Lime1.Opacity(0.4f)), }, - new SpriteIcon + votedIcon = new SpriteIcon { Size = new Vector2(16), Icon = FontAwesome.Solid.ThumbsUp, - Colour = colours.Lime1, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding { Right = 10 }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, } } }, @@ -641,10 +640,9 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Width = 0.9f, Direction = FillDirection.Vertical, Spacing = new Vector2(2), - Padding = new MarginPadding(5), + Padding = new MarginPadding(5) { Right = 35 }, Children = new Drawable[] { new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) @@ -677,7 +675,8 @@ namespace osu.Game.Screens.Ranking voted.BindValueChanged(_ => { - votedIndicator.FadeTo(voted.Value ? 1 : 0, 250, Easing.OutQuint); + votedBackground.FadeColour(voted.Value ? colours.Lime2 : colours.Gray2, 250, Easing.OutQuint); + votedIcon.FadeColour(voted.Value ? Colour4.Black : Colour4.White, 250, Easing.OutQuint); }, true); FinishTransforms(true); } From ffed666b97387ecdd8aaa78ceef32c27ea735c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Apr 2025 14:29:50 +0200 Subject: [PATCH 079/281] Compromise between popover and persistent view with a slideout --- osu.Game/Screens/Ranking/UserTagControl.cs | 117 +++++++++++++++------ 1 file changed, 85 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index da3059aaf4..d95238807a 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -77,10 +77,10 @@ namespace osu.Game.Screens.Ranking Padding = new MarginPadding(10), ColumnDimensions = [ - new Dimension(GridSizeMode.Absolute, 350), - new Dimension() + new Dimension(), + new Dimension(GridSizeMode.AutoSize) ], - RowDimensions = [new Dimension(GridSizeMode.AutoSize, minSize: 250)], + RowDimensions = [new Dimension(GridSizeMode.AutoSize, minSize: 40)], Content = new[] { new Drawable[] @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, + Direction = FillDirection.Full, LayoutDuration = 300, LayoutEasing = Easing.OutQuint, Spacing = new Vector2(4), @@ -106,7 +106,6 @@ namespace osu.Game.Screens.Ranking }, new TagList { - RelativeSizeAxes = Axes.Both, AvailableTags = { BindTarget = allTagsById }, OnSelected = toggleVote, } @@ -436,7 +435,7 @@ namespace osu.Game.Screens.Ranking } else { - mainBackground.FadeColour(colours.Gray4, transition_duration, Easing.OutQuint); + mainBackground.FadeColour(colours.Gray6, transition_duration, Easing.OutQuint); tagCategoryText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); tagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint); @@ -452,6 +451,7 @@ namespace osu.Game.Screens.Ranking { private SearchTextBox searchBox = null!; private SearchContainer searchContainer = null!; + private Container content = null!; public BindableDictionary AvailableTags { get; } = new BindableDictionary(); @@ -459,47 +459,81 @@ namespace osu.Game.Screens.Ranking private CancellationTokenSource? loadCancellationTokenSource; + private readonly BindableBool expanded = new BindableBool(); + [BackgroundDependencyLoader] private void load(OsuColour colours) { - Masking = true; - CornerRadius = 5; - + Margin = new MarginPadding { Left = 30 }; InternalChildren = new Drawable[] { - new Box + new OsuClickableContainer { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopRight, + X = 10, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray5, + }, + new SpriteIcon + { + Size = new Vector2(16), + Icon = FontAwesome.Solid.Plus, + Margin = new MarginPadding(10), + } + }, + Action = expanded.Toggle, }, new Container { RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, Children = new Drawable[] { - searchBox = new SearchTextBox - { - HoldFocus = true, - RelativeSizeAxes = Axes.X, - Depth = float.MinValue, - Y = -2, // hacky compensation for masking issues - }, - new OsuScrollContainer + new Box { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 40, }, - ScrollbarOverlapsContent = false, - Child = searchContainer = new SearchContainer + Colour = colours.Gray5, + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10) { Top = 12 }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10, Bottom = 10 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - } - } + searchBox = new SearchTextBox + { + HoldFocus = true, + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, + Y = -2, // hacky compensation for masking issues + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 42, }, + ScrollbarOverlapsContent = false, + Child = searchContainer = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 5, Bottom = 10 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + } + } + }, + }, }, - } + }, }; } @@ -519,6 +553,25 @@ namespace osu.Game.Screens.Ranking }, loadCancellationTokenSource.Token); }, true); searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); + expanded.BindValueChanged(_ => + { + const float transition_duration = 250; + + if (expanded.Value) + { + this.ResizeWidthTo(400, transition_duration, Easing.OutQuint); + content.FadeIn(250, Easing.OutQuint); + RelativeSizeAxes = Axes.None; + this.ResizeHeightTo(300, transition_duration, Easing.OutQuint); + } + else + { + this.ResizeWidthTo(10, transition_duration, Easing.OutQuint); + content.FadeOut(250, Easing.OutQuint); + RelativeSizeAxes = Axes.Y; + this.ResizeHeightTo(1, transition_duration, Easing.OutQuint); + } + }, true); } private IEnumerable createItems(IEnumerable tags) @@ -611,7 +664,7 @@ namespace osu.Game.Screens.Ranking new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Gray6, + Colour = colours.Gray7, Depth = float.MaxValue, }, new Container From b614887e268b43e0a26254ff4604f234b8a81b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Apr 2025 14:30:30 +0200 Subject: [PATCH 080/281] Fix corner radii not matching --- osu.Game/Screens/Ranking/UserTagControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index d95238807a..ac30482687 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -324,7 +324,7 @@ namespace osu.Game.Screens.Ranking { Anchor = Anchor.CentreLeft; Origin = Anchor.CentreLeft; - CornerRadius = 8; + CornerRadius = 5; Masking = true; EdgeEffect = new EdgeEffectParameters { From 33c4c142b776f83817e6ed4b4e1a3b4b23242f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Apr 2025 14:34:24 +0200 Subject: [PATCH 081/281] Adjust font weights --- osu.Game/Screens/Ranking/UserTagControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index ac30482687..ab8443f69c 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -370,6 +370,7 @@ namespace osu.Game.Screens.Ranking tagNameText = new OsuSpriteText { Text = UserTag.DisplayName, + Font = OsuFont.Default.With(weight: FontWeight.SemiBold), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Margin = new MarginPadding { Horizontal = 6 } @@ -698,7 +699,7 @@ namespace osu.Game.Screens.Ranking Padding = new MarginPadding(5) { Right = 35 }, Children = new Drawable[] { - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)) + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, From e4147f4f0b911458256ed048b46797ad5e62f9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Apr 2025 14:35:28 +0200 Subject: [PATCH 082/281] Adjust glow --- osu.Game/Screens/Ranking/UserTagControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index ab8443f69c..289b7b3ecd 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -329,7 +329,7 @@ namespace osu.Game.Screens.Ranking EdgeEffect = new EdgeEffectParameters { Colour = colours.Lime1, - Radius = 5, + Radius = 6, Type = EdgeEffectType.Glow, }; Content.AddRange(new Drawable[] @@ -432,7 +432,7 @@ namespace osu.Game.Screens.Ranking mainBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint); tagCategoryText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); tagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); - FadeEdgeEffectTo(0.5f, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0.3f, transition_duration, Easing.OutQuint); } else { From 71f55928b69c08a04dc14406b4fc9b9416781bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Apr 2025 09:56:30 +0200 Subject: [PATCH 083/281] Add test coverage --- .../ManiaFilterCriteriaTest.cs | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs new file mode 100644 index 0000000000..3c6046a986 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class ManiaFilterCriteriaTest + { + [TestCase] + public void TestKeysEqualSingleValue() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1"); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria + { + Mods = [new ManiaModKey1()] + })); + } + + [TestCase] + public void TestKeysEqualMultipleValues() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1,3,5,7"); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria + { + Mods = [new ManiaModKey1()] + })); + } + + [TestCase] + public void TestKeysNotEqualSingleValue() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1"); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria + { + Mods = [new ManiaModKey1()] + })); + } + + [TestCase] + public void TestKeysNotEqualMultipleValues() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1,3,5,7"); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria + { + Mods = [new ManiaModKey1()] + })); + } + + [TestCase] + public void TestKeysGreaterOrEqualThan() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4"); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria + { + Mods = [new ManiaModKey7()] + })); + } + + [TestCase] + public void TestFilterIntersection() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4"); + criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "7"); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 7 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 9 }), + new FilterCriteria())); + } + + [TestCase] + public void TestInvalidFilters() + { + var criteria = new ManiaFilterCriteria(); + + Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text")); + Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text")); + Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6")); + } + } +} From 36539f1947c0a3f627ee8322815d1d68c9f5a1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Apr 2025 09:56:38 +0200 Subject: [PATCH 084/281] Define constant for maximum mania key count --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 765f2be345..e3ac0e1a3d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -36,6 +36,11 @@ namespace osu.Game.Beatmaps.Formats /// public const double CONTROL_POINT_LENIENCY = 5; + /// + /// The maximum allowed number of keys in mania beatmaps. + /// + public const int MAX_MANIA_KEY_COUNT = 18; + internal static RulesetStore? RulesetStore; private Beatmap beatmap = null!; @@ -116,7 +121,7 @@ namespace osu.Game.Beatmaps.Formats // mania uses "circle size" for key count, thus different allowable range difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3 ? Math.Clamp(difficulty.CircleSize, 0, 10) - : Math.Clamp(difficulty.CircleSize, 1, 18); + : Math.Clamp(difficulty.CircleSize, 1, MAX_MANIA_KEY_COUNT); difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10); difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10); From 446b26c1e6af90a635961192cf1a663a656223eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Apr 2025 09:56:43 +0200 Subject: [PATCH 085/281] Simplify filter logic --- .../ManiaFilterCriteria.cs | 123 +++++++----------- 1 file changed, 48 insertions(+), 75 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 61ffa91626..63ef99328b 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Mods; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania { public class ManiaFilterCriteria : IRulesetFilterCriteria { - private readonly HashSet includedKeyCounts = Enumerable.Range(1, 20).ToHashSet(); + private readonly HashSet includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet(); public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { @@ -32,85 +33,57 @@ namespace osu.Game.Rulesets.Mania { case "key": case "keys": - if (op == Operator.Equal) - { - // If the filter is empty - if (includedKeyCounts.Count == 20) - { - includedKeyCounts.Clear(); + { + var keyCounts = new HashSet(); - foreach (string strValue in strValues.Split(',')) - { - if (int.TryParse(strValue, out int value)) - { - includedKeyCounts.Add(value); - } - else - { - return false; - } - } - } - else - { - HashSet tmp = new HashSet(); - - foreach (string strValue in strValues.Split(',')) - { - if (int.TryParse(strValue, out int value)) - { - tmp.Add(value); - } - else - { - return false; - } - } - - includedKeyCounts.IntersectWith(tmp); - } - } - else if (op == Operator.NotEqual) + foreach (string strValue in strValues.Split(',')) { - foreach (string strValue in strValues.Split(',')) - { - if (int.TryParse(strValue, out int value)) - { - includedKeyCounts.Remove(value); - } - else - { - return false; - } - } - } - else - { - if (!int.TryParse(strValues, out int value)) - { + if (!int.TryParse(strValue, out int keyCount)) return false; - } - switch (op) - { - case Operator.Less: - includedKeyCounts.RemoveWhere(k => k >= value); - break; - case Operator.LessOrEqual: - includedKeyCounts.RemoveWhere(k => k > value); - break; - case Operator.Greater: - includedKeyCounts.RemoveWhere(k => k <= value); - break; - case Operator.GreaterOrEqual: - includedKeyCounts.RemoveWhere(k => k < value); - break; - default: - return false; - } + keyCounts.Add(keyCount); } - break; + int? singleKeyCount = keyCounts.Count == 1 ? keyCounts.Single() : null; + + switch (op) + { + case Operator.Equal: + includedKeyCounts.IntersectWith(keyCounts); + return true; + + case Operator.NotEqual: + includedKeyCounts.ExceptWith(keyCounts); + return true; + + case Operator.Less: + if (singleKeyCount == null) return false; + + includedKeyCounts.RemoveWhere(k => k >= singleKeyCount.Value); + return true; + + case Operator.LessOrEqual: + if (singleKeyCount == null) return false; + + includedKeyCounts.RemoveWhere(k => k > singleKeyCount.Value); + return true; + + case Operator.Greater: + if (singleKeyCount == null) return false; + + includedKeyCounts.RemoveWhere(k => k <= singleKeyCount.Value); + return true; + + case Operator.GreaterOrEqual: + if (singleKeyCount == null) return false; + + includedKeyCounts.RemoveWhere(k => k < singleKeyCount.Value); + return true; + + default: + return false; + } + } default: return false; @@ -121,7 +94,7 @@ namespace osu.Game.Rulesets.Mania public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { - if (includedKeyCounts.Count != 20) + if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT) { // Interpreting as the Mod type is required for equality comparison. HashSet oldSet = mods.OldValue.OfType().AsEnumerable().ToHashSet(); From aa58fa58cb6f2d5a3679a60e5f8fb5f44fd7a8e0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Apr 2025 17:59:52 +0900 Subject: [PATCH 086/281] Add deduping for active mods, add documentation --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index c66e1a906c..692ef0fd2f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -91,7 +91,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (!newUserMods.SequenceEqual(SelectedMods.Value)) SelectedMods.Value = newUserMods; - ActiveMods.Value = ComputeActiveMods(); + // The active mods include the playlist item's required mods which change separately from the selected mods. + IReadOnlyList newActiveMods = ComputeActiveMods(); + if (!newActiveMods.SequenceEqual(ActiveMods.Value)) + ActiveMods.Value = ComputeActiveMods(); } protected override IReadOnlyList ComputeActiveMods() From d0de8e908d4cc64f8d9ebd4b155f37f9fdaf6214 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Apr 2025 18:27:16 +0900 Subject: [PATCH 087/281] Fix duplicate `ComputeActiveMods()` call --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index 692ef0fd2f..dc443f595b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -94,7 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // The active mods include the playlist item's required mods which change separately from the selected mods. IReadOnlyList newActiveMods = ComputeActiveMods(); if (!newActiveMods.SequenceEqual(ActiveMods.Value)) - ActiveMods.Value = ComputeActiveMods(); + ActiveMods.Value = newActiveMods; } protected override IReadOnlyList ComputeActiveMods() From 56169d7ac4cb641e22d3f3abfb6bfcca7e10e100 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Apr 2025 19:58:31 +0900 Subject: [PATCH 088/281] Add failing test --- .../TestSceneMultiplayerMatchSubScreen.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e51ea12e83..14e6a67d3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -336,6 +337,61 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0)); } + [Test] + public void TestUserModSelectUpdatesWhenNotVisible() + { + AddStep("add playlist item", () => + { + room.Playlist = + [ + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = [new APIMod(new OsuModFlashlight())] + } + ]; + }); + + ClickButtonWhenEnabled(); + AddUntilStep("wait for join", () => RoomJoined); + + // 1. Open the mod select overlay and enable flashlight + + ClickButtonWhenEnabled(); + AddUntilStep("mod select contents loaded", () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); + AddStep("click flashlight panel", () => + { + ModPanel panel = this.ChildrenOfType().Single(p => p.Mod is OsuModFlashlight); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("flashlight mod enabled", () => MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + + // 2. Close the mod select overlay, edit the playlist to disable allowed mods, and then edit it again to re-enable allowed mods. + + AddStep("close mod select overlay", () => this.ChildrenOfType().Single().Hide()); + AddUntilStep("mod select overlay not present", () => !this.ChildrenOfType().Single().IsPresent); + AddStep("disable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0]) + { + AllowedMods = [] + }))); + // This would normally be done as part of the above operation with an actual server. + AddStep("disable user mods", () => MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, Array.Empty())); + AddUntilStep("flashlight mod disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + AddStep("re-enable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0]) + { + AllowedMods = [new APIMod(new OsuModFlashlight())] + }))); + AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + + // 3. Open the mod select overlay, check that the flashlight mod panel is deactivated. + + ClickButtonWhenEnabled(); + AddUntilStep("mod select contents loaded", () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); + AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + AddAssert("flashlight mod panel not activated", () => !this.ChildrenOfType().Single(p => p.Mod is OsuModFlashlight).Active.Value); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] From f120684b145f5f6b9892eca1566f22b13ba6f210 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 2 Apr 2025 20:02:39 +0900 Subject: [PATCH 089/281] Fix skipped updates leading to incorrect validation --- .../Multiplayer/Match/MultiplayerUserModSelectOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs index dc443f595b..8463a4720c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -44,7 +44,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateValidMods(); } - private void onRoomUpdated() => Scheduler.AddOnce(updateValidMods); + private void onRoomUpdated() + { + // Importantly, this is not scheduled because the client must not skip intermediate server states to validate the allowed mods. + updateValidMods(); + } private void onSelectedModsChanged(ValueChangedEvent> mods) { From 22f2c6f7b97fe748844726adf5b914dab6feb42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Apr 2025 12:39:45 +0200 Subject: [PATCH 090/281] Add support for ruleset-specific user tags Follow-up from https://github.com/ppy/osu-web/pull/12059 --- osu.Game/Online/API/Requests/Responses/APITag.cs | 3 +++ osu.Game/Screens/Ranking/UserTagControl.cs | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APITag.cs b/osu.Game/Online/API/Requests/Responses/APITag.cs index 4dd18663af..b0454fdb1d 100644 --- a/osu.Game/Online/API/Requests/Responses/APITag.cs +++ b/osu.Game/Online/API/Requests/Responses/APITag.cs @@ -15,5 +15,8 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("description")] public string Description { get; set; } = string.Empty; + + [JsonProperty("ruleset_id")] + public int? RulesetId { get; set; } } } diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index ae4a918ae5..80d487112b 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -148,12 +148,14 @@ namespace osu.Game.Screens.Ranking if (allTags.Value == null || apiBeatmap.Value?.TopTags == null) return; - var allTagsById = allTags.Value.ToDictionary(t => t.Id); + var relevantTagsById = allTags.Value + .Where(tag => tag.RulesetId == null || tag.RulesetId == beatmapInfo.Ruleset.OnlineID) + .ToDictionary(t => t.Id); var ownTagIds = apiBeatmap.Value.OwnTagIds?.ToHashSet() ?? new HashSet(); foreach (var topTag in apiBeatmap.Value.TopTags) { - if (allTagsById.Remove(topTag.TagId, out var tag)) + if (relevantTagsById.Remove(topTag.TagId, out var tag)) { displayedTags.Add(new UserTag(tag) { @@ -163,7 +165,7 @@ namespace osu.Game.Screens.Ranking } } - extraTags.AddRange(allTagsById.Select(t => new UserTag(t.Value))); + extraTags.AddRange(relevantTagsById.Select(t => new UserTag(t.Value))); loadingLayer.Hide(); } From db0f1895fb056e00efaacb3896fc44502518c6bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Apr 2025 13:25:18 +0200 Subject: [PATCH 091/281] Add rudimentary test coverage --- .../Visual/Ranking/TestSceneUserTagControl.cs | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index d622df8d76..9174b2a3db 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -9,6 +9,8 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Ranking; namespace osu.Game.Tests.Visual.Ranking @@ -20,10 +22,6 @@ namespace osu.Game.Tests.Visual.Ranking [SetUpSteps] public void SetUpSteps() { - AddStep("set up working beatmap", () => - { - Beatmap.Value.BeatmapInfo.OnlineID = 42; - }); AddStep("set up network requests", () => { dummyAPI.HandleRequest = request => @@ -40,6 +38,7 @@ namespace osu.Game.Tests.Visual.Ranking new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", }, new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", }, new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", }, + new APITag { Id = 5, Name = "mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, }, ] }), 500); return true; @@ -67,19 +66,34 @@ namespace osu.Game.Tests.Visual.Ranking return false; }; }); - AddStep("create control", () => + AddStep("show for osu! beatmap", () => { - Child = new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - Child = new UserTagControl(Beatmap.Value.BeatmapInfo) - { - Width = 500, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }; + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + working.BeatmapInfo.OnlineID = 42; + Beatmap.Value = working; + recreateControl(); }); + AddStep("show for taiko beatmap", () => + { + var working = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo); + working.BeatmapInfo.OnlineID = 44; + Beatmap.Value = working; + recreateControl(); + }); + } + + private void recreateControl() + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new UserTagControl(Beatmap.Value.BeatmapInfo) + { + Width = 500, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; } } } From dbd2fa63cddcaa5a52be189c0b7b23007c9fe0a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 14:10:12 +0900 Subject: [PATCH 092/281] Fix letterbox showing above playfield border Closes #32652. No comment. --- osu.Game/Screens/Play/Player.cs | 41 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a738a40993..612d66a896 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -135,6 +135,8 @@ namespace osu.Game.Screens.Play public BreakOverlay BreakOverlay; + private LetterboxOverlay letterboxOverlay; + /// /// Whether the gameplay is currently in a break. /// @@ -277,6 +279,12 @@ namespace osu.Game.Screens.Play var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); GameplayClockContainer.Add(new GameplayScrollWheelHandling()); + // needs to exist in frame stable content, but is used by underlay layers so make sure assigned early. + breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Breaks = Beatmap.Value.Beatmap.Breaks + }; + // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. GameplayClockContainer.Add(rulesetSkinProvider); @@ -292,7 +300,7 @@ namespace osu.Game.Screens.Play Children = new[] { // underlay and gameplay should have access to the skinning sources. - createUnderlayComponents(), + createUnderlayComponents(Beatmap.Value), createGameplayComponents(Beatmap.Value) } }, @@ -335,10 +343,13 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(DrawableRuleset.FrameStableClock); dependencies.CacheAs(DrawableRuleset.FrameStableClock); + letterboxOverlay.Clock = DrawableRuleset.FrameStableClock; + letterboxOverlay.ProcessCustomClock = false; + // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. - failAnimationContainer.Add(createOverlayComponents(Beatmap.Value)); + failAnimationContainer.Add(createOverlayComponents()); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -409,14 +420,22 @@ namespace osu.Game.Screens.Play protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); - private Drawable createUnderlayComponents() + private Drawable createUnderlayComponents(WorkingBeatmap working) { var container = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both }, + DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) + { + RelativeSizeAxes = Axes.Both + }, + letterboxOverlay = new LetterboxOverlay + { + BreakTracker = breakTracker, + Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, + }, new KiaiGameplayFountains(), }, }; @@ -434,15 +453,12 @@ namespace osu.Game.Screens.Play ScoreProcessor, HealthProcessor, new ComboEffects(ScoreProcessor), - breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Breaks = working.Beatmap.Breaks - } + breakTracker, }), } }; - private Drawable createOverlayComponents(IWorkingBeatmap working) + private Drawable createOverlayComponents() { var container = new Container { @@ -450,13 +466,6 @@ namespace osu.Game.Screens.Play Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), - new LetterboxOverlay - { - Clock = DrawableRuleset.FrameStableClock, - ProcessCustomClock = false, - BreakTracker = breakTracker, - Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, - }, HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { HoldToQuit = From 21c675633640e45e4af3106e9480bb6d99d67e48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 14:30:47 +0900 Subject: [PATCH 093/281] Fix timeout during diffcalc causing batch import to bail Closes https://github.com/ppy/osu/discussions/32628. --- osu.Game/Database/RealmArchiveModelImporter.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index e538530b79..a3cdc2dc77 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -139,9 +139,14 @@ namespace osu.Game.Database notification.Progress = (float)current / tasks.Length; } } - catch (OperationCanceledException) + catch (OperationCanceledException cancelled) { - throw; + // We don't want to abort the full import process based off difficulty calculator's internal cancellation + // see https://github.com/ppy/osu/blob/91f3be5feaab0c73c17e1a8c270516aa9bee1e14/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs#L65. + if (cancelled.CancellationToken == notification.CancellationToken) + throw; + + Logger.Error(cancelled, $@"Timed out importing ({task})", LoggingTarget.Database); } catch (Exception e) { From 6ef3bf50e3977e9275b2054210753d6bb074453a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 14:33:52 +0900 Subject: [PATCH 094/281] Avoid writing out team acronyms to JSON As proposed in https://github.com/ppy/osu/discussions/32626. --- osu.Game.Tournament/Models/TournamentMatch.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs index 0a700eb4d6..1d91febd1a 100644 --- a/osu.Game.Tournament/Models/TournamentMatch.cs +++ b/osu.Game.Tournament/Models/TournamentMatch.cs @@ -19,6 +19,7 @@ namespace osu.Game.Tournament.Models { public int ID; + [JsonIgnore] public List Acronyms { get From 11368b628bb9b7c4c3e1537fea89f09d03b42272 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 15:12:07 +0900 Subject: [PATCH 095/281] Fix metronome BPM text not matching expectations due to custom rounding implementation --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 34 +++++++++++-------- osu.Game/Screens/Edit/Timing/TimingSection.cs | 6 ++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index f3bd9ff257..26fb449196 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -7,7 +7,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -20,7 +19,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; -using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -217,7 +215,7 @@ namespace osu.Game.Screens.Edit.Timing bpmText = new OsuTextFlowContainer(st => { st.Font = OsuFont.Default.With(fixedWidth: true); - st.Spacing = new Vector2(-2.2f, 0); + st.Spacing = new Vector2(-1.9f, 0); }) { Name = @"BPM display", @@ -233,8 +231,7 @@ namespace osu.Game.Screens.Edit.Timing } private double effectiveBeatLength; - - private double effectiveBpm => 60_000 / effectiveBeatLength; + private double effectiveBpm; private TimingControlPoint timingPoint = null!; @@ -268,19 +265,25 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { - int intPart = (int)interpolatedBpm.Value; + string text = interpolatedBpm.Value.ToString("N2"); + int? breakPoint = null; - bpmText.Text = intPart.ToLocalisableString(); - - // While interpolating between two integer values, showing the decimal places would look a bit odd - // so rounding is applied until we're close to the final value. - int decimalPlaces = FormatUtils.FindPrecision((decimal)effectiveBpm); - - if (decimalPlaces > 0) + for (int i = 0; i < text.Length; i++) { - bool reachedFinalNumber = intPart == (int)effectiveBpm; + if (!char.IsDigit(text[i])) + breakPoint = i; + } - bpmText.AddText((effectiveBpm % 1).ToLocalisableString("." + new string('0', decimalPlaces)), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.1f); + if (breakPoint != null) + { + bool reachedFinalNumber = (int)interpolatedBpm.Value == (int)effectiveBpm; + + bpmText.Text = text.Substring(0, breakPoint.Value); + bpmText.AddText(text.Substring(breakPoint.Value), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.2f); + } + else + { + bpmText.Text = text; } } @@ -300,6 +303,7 @@ namespace osu.Game.Screens.Edit.Timing if (effectiveBeatLength != timingPoint.BeatLength / Divisor) { effectiveBeatLength = timingPoint.BeatLength / Divisor; + effectiveBpm = TimingSection.BeatLengthToBpm(effectiveBeatLength); EarlyActivationMilliseconds = timingPoint.BeatLength / 2; diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index ae1ac02dd6..0c06a4e69b 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Edit.Timing try { if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0) - beatLengthBindable.Value = beatLengthToBpm(doubleVal); + beatLengthBindable.Value = BeatLengthToBpm(doubleVal); } catch { @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Edit.Timing beatLengthBindable.BindValueChanged(val => { - Current.Value = beatLengthToBpm(val.NewValue).ToString("N2"); + Current.Value = BeatLengthToBpm(val.NewValue).ToString("N2"); }, true); } @@ -146,6 +146,6 @@ namespace osu.Game.Screens.Edit.Timing } } - private static double beatLengthToBpm(double beatLength) => 60000 / beatLength; + public static double BeatLengthToBpm(double beatLength) => 60000 / beatLength; } } From ce288023fe610e8ff7ae1a17729e92a57ba0d0ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 15:31:24 +0900 Subject: [PATCH 096/281] Change editor to save metadata changes without explicit textbox commit Closes https://github.com/ppy/osu/issues/32365. --- .../Editing/TestSceneMetadataSection.cs | 6 ++--- .../Screens/Edit/Setup/MetadataSection.cs | 22 +++++-------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs index 743529d40c..995acd28dd 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneMetadataSection.cs @@ -65,10 +65,10 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Keys(PlatformAction.Paste); }); - assertArtistMetadata("Example Artist"); + assertArtistMetadata("Example ArtistExample Artist"); // It's important values are committed immediately on focus loss so the editor exit sequence detects them. - AddAssert("value immediately changed on focus loss", () => + AddAssert("value still changed after focus loss", () => { ((IFocusManager)InputManager).TriggerFocusContention(metadataSection); return editorBeatmap.Metadata.Artist; @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Editing InputManager.Keys(PlatformAction.Paste); }); - assertArtistMetadata("Example Artist"); + assertArtistMetadata("Example ArtistExample Artist"); AddStep("commit", () => InputManager.Key(Key.Enter)); diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 85247bc15a..50e161db55 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -68,7 +67,11 @@ namespace osu.Game.Screens.Edit.Setup TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); foreach (var item in Children.OfType()) - item.OnCommit += onCommit; + { + // Apply immediately on any change to ensure that if the user hits Ctrl+S after making a change (without committing) + // it will still apply to the beatmap. + item.Current.BindValueChanged(_ => applyMetadata()); + } updateReadOnlyState(); } @@ -87,15 +90,6 @@ namespace osu.Game.Screens.Edit.Setup RomanisedTitleTextBox.ReadOnly = MetadataUtils.IsRomanised(TitleTextBox.Current.Value); } - private void onCommit(TextBox sender, bool newText) - { - if (!newText) return; - - // for now, update on commit rather than making BeatmapMetadata bindables. - // after switching database engines we can reconsider if switching to bindables is a good direction. - setMetadata(); - } - private void reloadMetadata() { var metadata = Beatmap.Metadata; @@ -115,20 +109,16 @@ namespace osu.Game.Screens.Edit.Setup updateReadOnlyState(); } - private void setMetadata() + private void applyMetadata() { Beatmap.Metadata.ArtistUnicode = ArtistTextBox.Current.Value; Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value; - Beatmap.Metadata.TitleUnicode = TitleTextBox.Current.Value; Beatmap.Metadata.Title = RomanisedTitleTextBox.Current.Value; - Beatmap.Metadata.Author.Username = creatorTextBox.Current.Value; Beatmap.BeatmapInfo.DifficultyName = difficultyTextBox.Current.Value; Beatmap.Metadata.Source = sourceTextBox.Current.Value; Beatmap.Metadata.Tags = tagsTextBox.Current.Value; - - Beatmap.SaveState(); } private partial class FormRomanisedTextBox : FormTextBox From f9ceb59d70d6527080ee1448e22326680fc85456 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 15:48:03 +0900 Subject: [PATCH 097/281] Syncrhonise tranforms between arrow and circle pieces --- .../Objects/Drawables/DrawableOsuHitObject.cs | 13 +++++++++++++ .../Objects/Drawables/DrawableSliderRepeat.cs | 14 +++----------- .../Objects/Drawables/DrawableSliderTail.cs | 9 +-------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index b3a68ec92d..b5780f6d8c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -149,5 +149,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected float CalculateDrawableRelativePosition(Drawable drawable) => (drawable.ScreenSpaceDrawQuad.Centre.X - parentScreenSpaceRectangle.X) / parentScreenSpaceRectangle.Width; protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); + + protected void ApplyRepeatFadeIn(Drawable target) + { + DrawableSlider slider = (DrawableSlider)ParentHitObject; + + // When snaking in is enabled, the first end circle needs to be delayed until the snaking completes. + bool delayFadeIn = slider!.SliderBody?.SnakingIn.Value == true && ((SliderEndCircle)HitObject).RepeatIndex == 0; + + target + .FadeOut() + .Delay(delayFadeIn ? (slider.HitObject.TimePreempt) / 3 : 0) + .FadeIn(150); + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index e0fd7953f1..76be8dc6e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; - private double animDuration; - public SkinnableDrawable CirclePiece { get; private set; } public SkinnableDrawable Arrow { get; private set; } @@ -87,21 +85,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateInitialTransforms() { - // When snaking in is enabled, the first end circle needs to be delayed until the snaking completes. - bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0; - - animDuration = Math.Min(300, HitObject.SpanDuration); - - this - .FadeOut() - .Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0) - .FadeIn(150); + ApplyRepeatFadeIn(this); } protected override void UpdateHitStateTransforms(ArmedState state) { base.UpdateHitStateTransforms(state); + double animDuration = Math.Min(300, HitObject.SpanDuration); + switch (state) { case ArmedState.Idle: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 8bb1b0aebc..b41dae0731 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -85,14 +85,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); - - // When snaking in is enabled, the first end circle needs to be delayed until the snaking completes. - bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0; - - CirclePiece - .FadeOut() - .Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0) - .FadeIn(HitObject.TimeFadeIn); + ApplyRepeatFadeIn(CirclePiece); } protected override void UpdateHitStateTransforms(ArmedState state) From b47175081222330f580f4e7dbe4a7ef3f54fa046 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 16:17:29 +0900 Subject: [PATCH 098/281] Remove left-over scheduling thing --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index db01da730f..265d4efac8 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -15,7 +15,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Lists; -using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; @@ -667,14 +666,6 @@ namespace osu.Game.Rulesets.Objects.Drawables UpdateResult(false); } - /// - /// Schedules an to this . - /// - /// - /// Only provided temporarily until hitobject pooling is implemented. - /// - protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action); - /// /// An offset prior to the start time of at which this may begin displaying contents. /// By default, s are assumed to display their contents within 10 seconds prior to the start time of . From 9a4371a81748b821ae452f93c52763043d670097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Apr 2025 10:03:06 +0200 Subject: [PATCH 099/281] Add `[JsonIgnore]` to `MultiplayerRoom.CurrentPlaylistItem` The lack of this is currently failing a unit test on `osu-server-spectator` current master: https://github.com/ppy/osu-server-spectator/actions/runs/14193158383/job/39762243965#step:4:28 I don't think the failure actually matters because I don't think we're using json serialisation on spectator server side anywhere (used to for iOS at least, but I don't think we do anymore?), but probably better to be safe than sorry. --- osu.Game/Online/Multiplayer/MultiplayerRoom.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index db1722af8c..3c02565fa1 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -85,6 +85,7 @@ namespace osu.Game.Online.Multiplayer /// Retrieves the active as determined by the room's current settings. /// [IgnoreMember] + [JsonIgnore] public MultiplayerPlaylistItem CurrentPlaylistItem => Playlist.Single(item => item.ID == Settings.PlaylistItemId); /// From 4cd2e5ba25399d903edb52602263fe4c79b7593b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 17:25:42 +0900 Subject: [PATCH 100/281] Adjust again to be closer to stable --- .../Objects/Drawables/DrawableOsuHitObject.cs | 4 ++-- .../Objects/Drawables/DrawableSliderRepeat.cs | 5 ++++- .../Objects/Drawables/DrawableSliderTail.cs | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index b5780f6d8c..5594a38301 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); - protected void ApplyRepeatFadeIn(Drawable target) + protected void ApplyRepeatFadeIn(Drawable target, double fadeTime) { DrawableSlider slider = (DrawableSlider)ParentHitObject; @@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables target .FadeOut() .Delay(delayFadeIn ? (slider.HitObject.TimePreempt) / 3 : 0) - .FadeIn(150); + .FadeIn(fadeTime); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 76be8dc6e9..9368c69ebd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -85,7 +85,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateInitialTransforms() { - ApplyRepeatFadeIn(this); + base.UpdateInitialTransforms(); + + ApplyRepeatFadeIn(CirclePiece, HitObject.TimeFadeIn); + ApplyRepeatFadeIn(Arrow, 150); } protected override void UpdateHitStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index b41dae0731..e9f6f105bb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -85,7 +85,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); - ApplyRepeatFadeIn(CirclePiece); + + ApplyRepeatFadeIn(CirclePiece, HitObject.TimeFadeIn); } protected override void UpdateHitStateTransforms(ArmedState state) From bb8a9c83453723cd3d4ad724fd733e83467b5c89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 17:41:13 +0900 Subject: [PATCH 101/281] Fix argon reverse arrow animating weirdly after hit --- osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index 1fbdbafec4..bb5499b1a5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -85,9 +85,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out)); + + // When hit, don't animate further. This avoids a scale being applied on a scale and looking very weird. + return; } - else - Scale = Vector2.One; + + Scale = Vector2.One; const float move_distance = -12; const float scale_amount = 1.3f; From a49f9be243db5c2801afc9372bf79bcbce8b485a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 18:02:57 +0900 Subject: [PATCH 102/281] Add failing test coverage of regressing video scenario --- .../Formats/LegacyStoryboardDecoderTest.cs | 23 +++++++++++++++++++ .../video-custom-alpha-transform.osb | 5 ++++ 2 files changed, 28 insertions(+) create mode 100644 osu.Game.Tests/Resources/video-custom-alpha-transform.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 821173c521..b10cce6a52 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -306,6 +306,29 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestVideoWithCustomFadeIn() + { + var decoder = new LegacyStoryboardDecoder(); + + using var resStream = TestResources.OpenResource("video-custom-alpha-transform.osb"); + using var stream = new LineBufferedReader(resStream); + + var storyboard = decoder.Decode(stream); + + Assert.Multiple(() => + { + Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1)); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf()); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678)); + Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().StartTime, Is.EqualTo(1500)); + Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().EndTime, Is.EqualTo(1600)); + + Assert.That(storyboard.EarliestEventTime, Is.Null); + Assert.That(storyboard.LatestEventTime, Is.Null); + }); + } + [Test] public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds() { diff --git a/osu.Game.Tests/Resources/video-custom-alpha-transform.osb b/osu.Game.Tests/Resources/video-custom-alpha-transform.osb new file mode 100644 index 0000000000..39fcf87c06 --- /dev/null +++ b/osu.Game.Tests/Resources/video-custom-alpha-transform.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +Video,-5678,"Video.avi",0,0 + F,0,1500,1600,0,1 From 102085668f84bd80f1717f101adc22fc7075e7fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 18:03:23 +0900 Subject: [PATCH 103/281] Fix video offset start time regression with `StartTime` calculation changes --- osu.Game/Storyboards/StoryboardSprite.cs | 2 +- osu.Game/Storyboards/StoryboardVideo.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 49fa5d85c3..e10edfefe1 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -24,7 +24,7 @@ namespace osu.Game.Storyboards public readonly StoryboardCommandGroup Commands = new StoryboardCommandGroup(); - public double StartTime + public virtual double StartTime { get { diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 14189a1a6c..fb4ac56e98 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -14,9 +14,11 @@ namespace osu.Game.Storyboards { // This is just required to get a valid StartTime based on the incoming offset. // Actual fades are handled inside DrawableStoryboardVideo for now. - Commands.AddAlpha(Easing.None, offset, offset, 0, 0); + StartTime = offset; } + public override double StartTime { get; } + public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this); } } From 448573449c111daefea2cb0bf56e6a2bd146729b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 18:17:26 +0900 Subject: [PATCH 104/281] Only show required number of decimal places (and fix final alpha levels) --- osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 26fb449196..f91a67a7e3 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -19,6 +19,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; +using osu.Game.Utils; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -265,7 +266,10 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { - string text = interpolatedBpm.Value.ToString("N2"); + bool reachedFinalNumber = interpolatedBpm.Value == effectiveBpm; + int decimalPlaces = Math.Min(2, FormatUtils.FindPrecision((decimal)effectiveBpm)); + + string text = interpolatedBpm.Value.ToString($"N{decimalPlaces}"); int? breakPoint = null; for (int i = 0; i < text.Length; i++) @@ -276,8 +280,6 @@ namespace osu.Game.Screens.Edit.Timing if (breakPoint != null) { - bool reachedFinalNumber = (int)interpolatedBpm.Value == (int)effectiveBpm; - bpmText.Text = text.Substring(0, breakPoint.Value); bpmText.AddText(text.Substring(breakPoint.Value), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.2f); } From 6d1fc0e2354e3297f6aff52ba0d9d67c117cbb71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 18:32:02 +0900 Subject: [PATCH 105/281] Add back clamping of animation durations on slider repeat runs --- .../Objects/Drawables/DrawableOsuHitObject.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 5594a38301..b29be97951 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -153,9 +154,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected void ApplyRepeatFadeIn(Drawable target, double fadeTime) { DrawableSlider slider = (DrawableSlider)ParentHitObject; + int repeatIndex = ((SliderEndCircle)HitObject).RepeatIndex; + + Debug.Assert(slider != null); // When snaking in is enabled, the first end circle needs to be delayed until the snaking completes. - bool delayFadeIn = slider!.SliderBody?.SnakingIn.Value == true && ((SliderEndCircle)HitObject).RepeatIndex == 0; + bool delayFadeIn = slider.SliderBody?.SnakingIn.Value == true && repeatIndex == 0; + + if (repeatIndex > 0) + fadeTime = Math.Min(slider.HitObject.SpanDuration, fadeTime); target .FadeOut() From f19fa73fb2e05c543df8b26c80e913fc5210fe79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 18:49:18 +0900 Subject: [PATCH 106/281] Ensure a state is saved when committing metadata to trigger pending changes flow --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 50e161db55..ef9657f32e 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -71,6 +71,11 @@ namespace osu.Game.Screens.Edit.Setup // Apply immediately on any change to ensure that if the user hits Ctrl+S after making a change (without committing) // it will still apply to the beatmap. item.Current.BindValueChanged(_ => applyMetadata()); + item.OnCommit += (_, newText) => + { + if (newText) + Beatmap.SaveState(); + }; } updateReadOnlyState(); From 332b08160388cc0eaef2d22f5197ce9e5ee910ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 18:55:45 +0900 Subject: [PATCH 107/281] Rename some variables --- .../Play/PlayerSettings/BeatmapOffsetControl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index c2cd09c56f..23ccb3311b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -63,11 +63,11 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private IGameplayClock? gameplayClock { get; set; } - private double lastPlayAverage; + private double lastPlayMedian; private double lastPlayBeatmapOffset; private HitEventTimingDistributionGraph? lastPlayGraph; - private SettingsButton? useAverageButton; + private SettingsButton? calibrateFromLastPlayButton; private IDisposable? beatmapOffsetSubscription; @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.PlayerSettings return; } - lastPlayAverage = median; + lastPlayMedian = median; lastPlayBeatmapOffset = Current.Value; LinkFlowContainer globalOffsetText; @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Height = 50, }, new AverageHitError(hitEvents), - useAverageButton = new SettingsButton + calibrateFromLastPlayButton = new SettingsButton { Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => @@ -247,7 +247,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (Current.Disabled) return; - Current.Value = lastPlayBeatmapOffset - lastPlayAverage; + Current.Value = lastPlayBeatmapOffset - lastPlayMedian; lastAppliedScore.Value = ReferenceScore.Value; }, }, @@ -281,8 +281,8 @@ namespace osu.Game.Screens.Play.PlayerSettings bool allow = allowOffsetAdjust; - if (useAverageButton != null) - useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); + if (calibrateFromLastPlayButton != null) + calibrateFromLastPlayButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayMedian, adjustmentSinceLastPlay, Current.Precision / 2); Current.Disabled = !allow; } From 2a3241fd487e27bdd1153d2edff8ec76d6e3bae7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 19:19:06 +0900 Subject: [PATCH 108/281] Adjust mouse down animation in a better way (and apply to small tag buttons too) --- .../Graphics/UserInterface/OsuAnimatedButton.cs | 4 +++- osu.Game/Screens/Ranking/UserTagControl.cs | 13 ++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index 0eec04541c..48d225de41 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -25,6 +25,8 @@ namespace osu.Game.Graphics.UserInterface private Color4 hoverColour = Color4.White.Opacity(0.1f); + protected float ScaleOnMouseDown { get; init; } = 0.75f; + /// /// The background colour of the while it is hovered. /// @@ -119,7 +121,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnMouseDown(MouseDownEvent e) { - Content.ScaleTo(0.75f, 2000, Easing.OutQuint); + Content.ScaleTo(ScaleOnMouseDown, 2000, Easing.OutQuint); return base.OnMouseDown(e); } diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 7b36077bb3..84774e8ad8 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -319,6 +319,8 @@ namespace osu.Game.Screens.Ranking voted.BindTo(userTag.Voted); AutoSizeAxes = Axes.Both; + + ScaleOnMouseDown = 0.95f; } [BackgroundDependencyLoader] @@ -654,6 +656,8 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + + ScaleOnMouseDown = 0.95f; } [Resolved] @@ -736,15 +740,6 @@ namespace osu.Game.Screens.Ranking }, true); FinishTransforms(true); } - - protected override bool OnMouseDown(MouseDownEvent e) - { - bool result = base.OnMouseDown(e); - // slightly dodgy way of overriding the amount of scale-on-click (the default is way too much in this case) - ClearTransforms(targetMember: nameof(Scale)); - Content.ScaleTo(0.95f, 2000, Easing.OutQuint); - return result; - } } } } From 769e0bd4ffc68677f6e17601c77ebeab711a5cde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 19:19:34 +0900 Subject: [PATCH 109/281] Remove flow animations They don't look great, so let's just get something out which doesn't involve things flying everywhere to start with. --- osu.Game/Screens/Ranking/UserTagControl.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 84774e8ad8..bfc54e8423 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -98,8 +98,6 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, - LayoutDuration = 300, - LayoutEasing = Easing.OutQuint, Spacing = new Vector2(4), }, }, @@ -326,8 +324,6 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; CornerRadius = 5; Masking = true; EdgeEffect = new EdgeEffectParameters From db2366b73621c9db4a1e8b28b4afdb0cf7f6e54f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 10:47:40 +0900 Subject: [PATCH 110/281] Remove unreachable code --- osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 63ef99328b..9b2700c6e8 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -84,12 +84,9 @@ namespace osu.Game.Rulesets.Mania return false; } } - - default: - return false; } - return true; + return false; } public bool FilterMayChangeFromMods(ValueChangedEvent> mods) From 286b3d9f5b46e203165dbe31ea6ed31e6bc1c810 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 10:31:06 +0900 Subject: [PATCH 111/281] Rewrite match subscreen to use full online state --- .../Multiplayer/TestSceneHostOnlyQueueMode.cs | 2 +- .../Multiplayer/TestSceneMultiplayer.cs | 15 +- .../TestSceneMultiplayerMatchSubScreen.cs | 10 +- .../Online/Multiplayer/MultiplayerClient.cs | 22 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 544 --------- .../Match/MultiplayerMatchSettingsOverlay.cs | 8 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 1065 ++++++++++++----- osu.Game/Users/UserActivity.cs | 7 + 8 files changed, 806 insertions(+), 867 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs index 55c9e8142f..7d3d30b9f9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneHostOnlyQueueMode.cs @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap())); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); - AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.Beatmap.OnlineID == otherBeatmap.OnlineID); + AddUntilStep("selected item is new beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == otherBeatmap.OnlineID); } private void addItem(Func beatmap) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8fc0250d04..8066ea1b94 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -443,7 +443,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item); + ((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); @@ -484,7 +484,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item); + ((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); @@ -525,7 +525,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("Enter song select", () => { var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; - ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item); + ((MultiplayerMatchSubScreen)currentSubScreen).ShowSongSelect(item); }); AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true); @@ -657,7 +657,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("invoke on back button", () => multiplayerComponents.OnBackButton()); - AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().UserModsSelectOverlay.State.Value == Visibility.Hidden); + AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); @@ -828,11 +828,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for join", () => multiplayerClient.RoomJoined); - AddAssert("local room has correct settings", () => - { - var localRoom = this.ChildrenOfType().Single().Room; - return localRoom.Name == multiplayerClient.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2; - }); + AddAssert("local room has correct name", () => this.ChildrenOfType().Single().Room.Name, () => Is.EqualTo(multiplayerClient.ServerSideRooms[0].Name)); + AddAssert("local room has correct playlist", () => this.ChildrenOfType().Single().Items.Single().ID, () => Is.EqualTo(2)); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 14e6a67d3a..660f84b4d6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("mod select contents loaded", () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); AddUntilStep("mod select contains only double time mod", - () => this.ChildrenOfType().Single().UserModsSelectOverlay + () => this.ChildrenOfType().Single() .ChildrenOfType() .SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime); } @@ -212,7 +212,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("press toggle mod select key", () => InputManager.Key(Key.F1)); - AddUntilStep("mod select shown", () => this.ChildrenOfType().Single().UserModsSelectOverlay.State.Value == Visibility.Visible); + AddUntilStep("mod select shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); } [Test] @@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("press toggle mod select key", () => InputManager.Key(Key.F1)); AddWaitStep("wait some", 3); - AddAssert("mod select not shown", () => this.ChildrenOfType().Single().UserModsSelectOverlay.State.Value == Visibility.Hidden); + AddAssert("mod select not shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); } [Test] @@ -307,10 +307,10 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("mod select shows unranked", () => this.ChildrenOfType().Single().Ranked.Value == false); AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); - AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType().Single(m => m.Mod is ModFlashlight).TriggerClick()); + AddStep("select flashlight", () => this.ChildrenOfType().Single().ChildrenOfType().Single(m => m.Mod is ModFlashlight).TriggerClick()); AddAssert("score multiplier = 1.35", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01)); - AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200); + AddStep("change flashlight setting", () => ((OsuModFlashlight)this.ChildrenOfType().Single().SelectedMods.Value.Single()).FollowDelay.Value = 1200); AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 57aaf68853..92fc8a3dcf 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -36,6 +36,21 @@ namespace osu.Game.Online.Multiplayer /// public virtual event Action? RoomUpdated; + /// + /// Invoked when a user's local style is changed. + /// + public event Action? UserStyleChanged; + + /// + /// Invoked when a user's local mods are changed. + /// + public event Action? UserModsChanged; + + /// + /// Invoked when the room's settings are changed. + /// + public event Action? SettingsChanged; + /// /// Invoked when a new user joins the room. /// @@ -710,7 +725,7 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - public Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId) + Task IMultiplayerClient.UserStyleChanged(int userId, int? beatmapId, int? rulesetId) { Scheduler.Add(() => { @@ -723,13 +738,14 @@ namespace osu.Game.Online.Multiplayer user.BeatmapId = beatmapId; user.RulesetId = rulesetId; + UserStyleChanged?.Invoke(user); RoomUpdated?.Invoke(); }, false); return Task.CompletedTask; } - public Task UserModsChanged(int userId, IEnumerable mods) + Task IMultiplayerClient.UserModsChanged(int userId, IEnumerable mods) { Scheduler.Add(() => { @@ -741,6 +757,7 @@ namespace osu.Game.Online.Multiplayer user.Mods = mods; + UserModsChanged?.Invoke(user); RoomUpdated?.Invoke(); }, false); @@ -907,6 +924,7 @@ namespace osu.Game.Online.Multiplayer APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId); APIRoom.AutoSkip = Room.Settings.AutoSkip; + SettingsChanged?.Invoke(settings); RoomUpdated?.Invoke(); } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs deleted file mode 100644 index c73a36617d..0000000000 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ /dev/null @@ -1,544 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.ComponentModel; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Screens; -using osu.Game.Audio; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Cursor; -using osu.Game.Online.API; -using osu.Game.Online.Rooms; -using osu.Game.Overlays; -using osu.Game.Overlays.Mods; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Menu; -using osu.Game.Screens.OnlinePlay.Match.Components; -using osu.Game.Screens.OnlinePlay.Multiplayer; -using osu.Game.Screens.OnlinePlay.Multiplayer.Match; -using osu.Game.Utils; -using Container = osu.Framework.Graphics.Containers.Container; - -namespace osu.Game.Screens.OnlinePlay.Match -{ - [Cached(typeof(IPreviewTrackOwner))] - public abstract partial class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner - { - public readonly Bindable SelectedItem = new Bindable(); - - public override bool? ApplyModTrackAdjustments => true; - - protected override BackgroundScreen CreateBackground() => new MultiplayerRoomBackgroundScreen(); - - public override bool DisallowExternalBeatmapRulesetChanges => true; - - /// - /// A container that provides controls for selection of user mods. - /// This will be shown/hidden automatically when applicable. - /// - protected Drawable UserModsSection = null!; - - /// - /// A container that provides controls for selection of the user style. - /// This will be shown/hidden automatically when applicable. - /// - protected Drawable UserStyleSection = null!; - - /// - /// A container that will display the user's style. - /// - protected Container UserStyleDisplayContainer = null!; - - private Sample? sampleStart; - - [Resolved(CanBeNull = true)] - private IOverlayManager? overlayManager { get; set; } - - [Resolved] - private MusicController music { get; set; } = null!; - - [Resolved] - private BeatmapManager beatmapManager { get; set; } = null!; - - [Resolved] - protected RulesetStore Rulesets { get; private set; } = null!; - - [Resolved] - protected IAPIProvider API { get; private set; } = null!; - - [Resolved(canBeNull: true)] - protected OnlinePlayScreen? ParentScreen { get; private set; } - - [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } = null!; - - [Resolved(canBeNull: true)] - protected IDialogOverlay? DialogOverlay { get; private set; } - - [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))] - private readonly MultiplayerBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker(); - - protected IBindable BeatmapAvailability => beatmapAvailabilityTracker.Availability; - - public readonly Room Room; - - internal ModSelectOverlay UserModsSelectOverlay { get; private set; } = null!; - - private IDisposable? userModsSelectOverlayRegistration; - private RoomSettingsOverlay settingsOverlay = null!; - private Drawable mainContent = null!; - - /// - /// Creates a new . - /// - /// The . - protected RoomSubScreen(Room room) - { - Room = room; - Padding = new MarginPadding { Top = Header.HEIGHT }; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); - - InternalChild = new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - beatmapAvailabilityTracker, - new MultiplayerRoomSounds(), - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 50) - }, - Content = new[] - { - // Padded main content (drawable room + main content) - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Horizontal = WaveOverlayContainer.WIDTH_PADDING, - Bottom = 30 - }, - Children = new[] - { - mainContent = new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 10) - }, - Content = new[] - { - new Drawable[] - { - new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = new MultiplayerRoomPanel(Room) - { - OnEdit = () => settingsOverlay.Show(), - } - } - }, - null, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. - }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(20), - Child = CreateMainContent(), - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - } - } - } - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - // Resolves 1px masking errors between the settings overlay and the room panel. - Padding = new MarginPadding(-1), - Child = settingsOverlay = CreateRoomSettingsOverlay(Room) - } - }, - }, - }, - // Footer - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d") // Temporary. - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(5), - Child = CreateFooter() - }, - } - } - } - } - } - } - }; - - LoadComponent(UserModsSelectOverlay = new MultiplayerUserModSelectOverlay()); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - SelectedItem.BindValueChanged(_ => updateSpecifics()); - - beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); - - userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay); - - Room.PropertyChanged += onRoomPropertyChanged; - updateSetupState(); - } - - private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(Room.RoomID)) - updateSetupState(); - } - - private void updateSetupState() - { - if (Room.RoomID == null) - { - // A new room is being created. - // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. - mainContent.Hide(); - settingsOverlay.Show(); - } - else - { - mainContent.Show(); - settingsOverlay.Hide(); - } - } - - protected virtual bool IsConnected => API.State.Value == APIState.Online; - - public override bool OnBackButton() - { - if (Room.RoomID == null) - { - if (!ensureExitConfirmed()) - return true; - - settingsOverlay.Hide(); - return base.OnBackButton(); - } - - if (UserModsSelectOverlay.State.Value == Visibility.Visible) - { - UserModsSelectOverlay.Hide(); - return true; - } - - if (settingsOverlay.State.Value == Visibility.Visible) - { - settingsOverlay.Hide(); - return true; - } - - return base.OnBackButton(); - } - - protected void ShowUserModSelect() => UserModsSelectOverlay.Show(); - - public override void OnEntering(ScreenTransitionEvent e) - { - base.OnEntering(e); - beginHandlingTrack(); - } - - public override void OnSuspending(ScreenTransitionEvent e) - { - // Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state. - updateSpecifics(); - - onLeaving(); - base.OnSuspending(e); - } - - public override void OnResuming(ScreenTransitionEvent e) - { - base.OnResuming(e); - - updateSpecifics(); - - beginHandlingTrack(); - } - - protected bool ExitConfirmed { get; private set; } - - public override bool OnExiting(ScreenExitEvent e) - { - if (!ensureExitConfirmed()) - return true; - - if (Room.RoomID != null) - PartRoom(); - - Mods.Value = Array.Empty(); - - onLeaving(); - - return base.OnExiting(e); - } - - /// - /// Parts from the current room. - /// - protected abstract void PartRoom(); - - private bool ensureExitConfirmed() - { - if (ExitConfirmed) - return true; - - if (!IsConnected) - return true; - - bool hasUnsavedChanges = Room.RoomID == null && Room.Playlist.Count > 0; - - if (DialogOverlay == null || !hasUnsavedChanges) - return true; - - // if the dialog is already displayed, block exiting until the user explicitly makes a decision. - if (DialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) - { - discardChangesDialog.Flash(); - return false; - } - - DialogOverlay.Push(new ConfirmDiscardChangesDialog(() => - { - ExitConfirmed = true; - settingsOverlay.Hide(); - this.Exit(); - })); - - return false; - } - - protected void StartPlay() - { - if (SelectedItem.Value is not PlaylistItem item) - return; - - item = item.With( - ruleset: GetGameplayRuleset().OnlineID, - beatmap: new Optional(GetGameplayBeatmap())); - - // User may be at song select or otherwise when the host starts gameplay. - // Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state. - if (!this.IsCurrentScreen()) - { - this.MakeCurrent(); - - Schedule(StartPlay); - return; - } - - sampleStart?.Play(); - - // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). - var targetScreen = (Screen?)ParentScreen ?? this; - - targetScreen.Push(CreateGameplayScreen(item)); - } - - /// - /// Creates the gameplay screen to be entered. - /// - /// The playlist item about to be played. - /// The screen to enter. - protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem); - - private void updateSpecifics() - { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) - return; - - var rulesetInstance = GetGameplayRuleset().CreateInstance(); - - Mod[] allowedMods = item.Freestyle - ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() - : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - int beatmapId = GetGameplayBeatmap().OnlineID; - var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); - Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); - UserModsSelectOverlay.Beatmap.Value = Beatmap.Value; - - Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray(); - Ruleset.Value = GetGameplayRuleset(); - - if (allowedMods.Length > 0) - UserModsSection.Show(); - else - { - UserModsSection.Hide(); - UserModsSelectOverlay.Hide(); - } - - if (item.Freestyle) - { - UserStyleSection.Show(); - - PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional(GetGameplayBeatmap())); - PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item; - - if (gameplayItem.Equals(currentItem)) - return; - - UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true) - { - AllowReordering = false, - AllowEditing = true, - RequestEdit = _ => OpenStyleSelection() - }; - } - else - UserStyleSection.Hide(); - } - - protected virtual APIMod[] GetGameplayMods() => SelectedItem.Value!.RequiredMods; - - protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!; - - protected virtual IBeatmapInfo GetGameplayBeatmap() => SelectedItem.Value!.Beatmap; - - protected abstract void OpenStyleSelection(); - - private void beginHandlingTrack() - { - Beatmap.BindValueChanged(applyLoopingToTrack, true); - } - - private void onLeaving() - { - UserModsSelectOverlay.Hide(); - endHandlingTrack(); - - previewTrackManager.StopAnyPlaying(this); - } - - private void endHandlingTrack() - { - Beatmap.ValueChanged -= applyLoopingToTrack; - cancelTrackLooping(); - } - - private void applyLoopingToTrack(ValueChangedEvent? _ = null) - { - if (!this.IsCurrentScreen()) - return; - - var track = Beatmap.Value?.Track; - - if (track != null) - { - Beatmap.Value!.PrepareTrackForPreview(true); - music.EnsurePlayingSomething(); - } - } - - private void cancelTrackLooping() - { - var track = Beatmap.Value?.Track; - - if (track != null) - track.Looping = false; - } - - /// - /// Creates the main centred content. - /// - protected abstract Drawable CreateMainContent(); - - /// - /// Creates the footer content. - /// - protected abstract Drawable CreateFooter(); - - /// - /// Creates the room settings overlay. - /// - /// The room to change the settings of. - protected abstract RoomSettingsOverlay CreateRoomSettingsOverlay(Room room); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - userModsSelectOverlayRegistration?.Dispose(); - Room.PropertyChanged -= onRoomPropertyChanged; - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 42d240c60e..018d36069e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -12,7 +12,6 @@ using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -35,7 +34,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private MatchSettings settings = null!; public MultiplayerMatchSettingsOverlay(Room room) @@ -274,11 +272,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.X, Height = 40, Text = "Select beatmap", - Action = () => - { - if (matchSubScreen.IsCurrentScreen()) - matchSubScreen.Push(new MultiplayerMatchSongSelect(matchSubScreen.Room)); - } + Action = () => matchSubScreen.ShowSongSelect() } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 0cc033907f..cff823c969 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -1,15 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Online; @@ -20,6 +26,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; +using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -28,258 +35,777 @@ using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Users; +using osu.Game.Utils; using osuTK; +using Container = osu.Framework.Graphics.Containers.Container; using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList; namespace osu.Game.Screens.OnlinePlay.Multiplayer { [Cached] - public partial class MultiplayerMatchSubScreen : RoomSubScreen, IHandlePresentBeatmap + public partial class MultiplayerMatchSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner, IHandlePresentBeatmap { + /// + /// Footer height. + /// + private const float footer_height = 50; + + /// + /// Padding between content and footer. + /// + private const float footer_padding = 30; + + /// + /// Internal padding of the content. + /// + private const float content_padding = 20; + + /// + /// Padding between columns of the content. + /// + private const float column_padding = 10; + + /// + /// Padding between rows of the content. + /// + private const float row_padding = 10; + public override string Title { get; } public override string ShortTitle => "room"; + public override bool? ApplyModTrackAdjustments => true; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + /// + /// Whether the user has confirmed they want to exit this screen in the presence of unsaved changes. + /// + protected bool ExitConfirmed { get; private set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private AudioManager audio { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + + [Resolved] + private MusicController music { get; set; } = null!; + + [Resolved] + private OnlinePlayScreen? parentScreen { get; set; } + + [Resolved] + private IOverlayManager? overlayManager { get; set; } + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] private MultiplayerClient client { get; set; } = null!; - [Resolved(canBeNull: true)] + [Resolved] private OsuGame? game { get; set; } - private AddItemButton addItemButton = null!; + [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))] + private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker(); + + private readonly Room room; + + private Drawable roomContent = null!; + private MultiplayerMatchSettingsOverlay settingsOverlay = null!; + + private FillFlowContainer userModsSection = null!; + private MultiplayerUserModSelectOverlay userModsSelectOverlay = null!; + + private FillFlowContainer userStyleSection = null!; + private Container userStyleDisplayContainer = null!; + + private Sample? sampleStart; + private IDisposable? userModsSelectOverlayRegistration; + + private long lastPlaylistItemId; + private bool isRoomJoined; public MultiplayerMatchSubScreen(Room room) - : base(room) { + this.room = room; + Title = room.RoomID == null ? "New room" : room.Name; Activity.Value = new UserActivity.InLobby(room); + + Padding = new MarginPadding { Top = Header.HEIGHT }; + } + + [BackgroundDependencyLoader] + private void load() + { + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + + InternalChild = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + beatmapAvailabilityTracker, + new MultiplayerRoomSounds(), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = WaveOverlayContainer.WIDTH_PADDING, + Bottom = footer_height + footer_padding + }, + Children = new[] + { + roomContent = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, row_padding), + }, + Content = new[] + { + new Drawable[] + { + new MultiplayerRoomPanel(room) + { + OnEdit = () => settingsOverlay.Show() + } + }, + null, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary. + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(content_padding), + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, column_padding), + new Dimension(), + new Dimension(GridSizeMode.Absolute, column_padding), + new Dimension(), + }, + Content = new[] + { + new Drawable?[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new ParticipantsListHeader() + }, + new Drawable[] + { + new ParticipantsList + { + RelativeSizeAxes = Axes.Both + }, + } + } + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new OverlinedHeader("Beatmap queue") + }, + new Drawable[] + { + new AddItemButton + { + RelativeSizeAxes = Axes.X, + Height = 40, + Text = "Add item", + Action = () => ShowSongSelect() + }, + }, + null, + new Drawable[] + { + new MultiplayerPlaylist + { + RelativeSizeAxes = Axes.Both, + RequestEdit = ShowSongSelect + } + }, + new Drawable[] + { + userModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UserModSelectButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Height = 30, + Text = "Select", + Action = showUserModSelect, + }, + new MultiplayerUserModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.8f), + }, + } + }, + } + } + }, + new Drawable[] + { + userStyleSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Alpha = 0, + Children = new Drawable[] + { + new OverlinedHeader("Difficulty"), + userStyleDisplayContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }, + }, + }, + }, + null, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new OverlinedHeader("Chat") + }, + new Drawable[] + { + new MatchChatDisplay(room) + { + RelativeSizeAxes = Axes.Both + } + } + } + } + } + } + } + } + } + } + } + }, + settingsOverlay = new MultiplayerMatchSettingsOverlay(room) + } + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = footer_height, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d") // Temporary. + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(5), + Child = new MultiplayerMatchFooter() + } + } + } + } + }; + + LoadComponent(userModsSelectOverlay = new MultiplayerUserModSelectOverlay + { + Beatmap = { BindTarget = Beatmap } + }); } protected override void LoadComplete() { base.LoadComplete(); - BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); + userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); - client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; + client.SettingsChanged += onSettingsChanged; + client.ItemChanged += onItemChanged; + client.UserStyleChanged += onUserStyleChanged; + client.UserModsChanged += onUserModsChanged; + client.LoadRequested += onLoadRequested; - if (!client.IsConnected.Value) - handleRoomLost(); + beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); + + onRoomUpdated(); + updateGameplayState(); + updateUserActivity(); } - protected override bool IsConnected => base.IsConnected && client.IsConnected.Value; - - protected override Drawable CreateMainContent() => new Container + /// + /// Responds to changes in the active room to adjust the visibility of the settings and main content. + /// Only the settings overlay is visible while the room isn't created, and only the main content is visible after creation. + /// + private void onRoomUpdated() => Scheduler.AddOnce(() => { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, - Child = new OsuContextMenuContainer + bool newIsRoomJoined = client.Room != null; + + if (newIsRoomJoined) { - RelativeSizeAxes = Axes.Both, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(), - }, - Content = new[] - { - new Drawable?[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] { new ParticipantsListHeader() }, - new Drawable[] - { - new ParticipantsList - { - RelativeSizeAxes = Axes.Both - }, - } - } - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] { new OverlinedHeader("Beatmap queue") }, - new Drawable[] - { - addItemButton = new AddItemButton - { - RelativeSizeAxes = Axes.X, - Height = 40, - Text = "Add item", - Action = () => OpenSongSelection() - }, - }, - null, - new Drawable[] - { - new MultiplayerPlaylist - { - RelativeSizeAxes = Axes.Both, - RequestEdit = OpenSongSelection - } - }, - new[] - { - UserModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Extra mods"), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UserModSelectButton - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 90, - Height = 30, - Text = "Select", - Action = ShowUserModSelect, - }, - new MultiplayerUserModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(0.8f), - }, - } - }, - } - } - }, - new[] - { - UserStyleSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 10 }, - Alpha = 0, - Children = new Drawable[] - { - new OverlinedHeader("Difficulty"), - UserStyleDisplayContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - }, - }, - }, - }, - null, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] { new OverlinedHeader("Chat") }, - new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } } - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } - }, - } - } - } + roomContent.Show(); + settingsOverlay.Hide(); } - }; + else if (isRoomJoined) + { + Logger.Log($"{this} exiting due to loss of room or connection"); + + if (this.IsCurrentScreen()) + this.Exit(); + else + ValidForResume = false; + } + else + { + Debug.Assert(!isRoomJoined && !newIsRoomJoined); + + // A new room is being created. + // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. + roomContent.Hide(); + settingsOverlay.Show(); + } + + isRoomJoined = newIsRoomJoined; + }); /// - /// Opens the song selection screen to add or edit an item. + /// Responds to changes in the room's settings to update the gameplay state and local user's activity. + /// + private void onSettingsChanged(MultiplayerRoomSettings settings) + { + if (settings.PlaylistItemId != lastPlaylistItemId) + { + updateGameplayState(); + lastPlaylistItemId = settings.PlaylistItemId; + } + + updateUserActivity(); + } + + /// + /// Responds to changes in the active playlist item to update the gameplay state. + /// + private void onItemChanged(MultiplayerPlaylistItem item) + { + if (item.ID == client.Room?.Settings.PlaylistItemId) + updateGameplayState(); + } + + /// + /// Responds to changes in the local user's style to update the gameplay state. + /// + private void onUserStyleChanged(MultiplayerRoomUser user) + { + if (user.Equals(client.LocalUser)) + updateGameplayState(); + } + + /// + /// Responds to changes in the local user's mods style to update the gameplay state. + /// + private void onUserModsChanged(MultiplayerRoomUser user) + { + if (user.Equals(client.LocalUser)) + updateGameplayState(); + } + + /// + /// Responds to notifications from the server that a gameplay session is ready to attempt to start the gameplay session. + /// + private void onLoadRequested() + { + if (client.Room == null || client.LocalUser == null) + return; + + // In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session. + // For now, we want to game to switch to the new game so need to request exiting from the play screen. + if (!parentScreen.IsCurrentScreen()) + { + parentScreen.MakeCurrent(); + Schedule(onLoadRequested); + return; + } + + if (!this.IsCurrentScreen()) + { + this.MakeCurrent(); + Schedule(onLoadRequested); + return; + } + + if (beatmapAvailabilityTracker.Availability.Value.State != DownloadState.LocallyAvailable) + return; + + sampleStart?.Play(); + + int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); + MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray(); + + // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). + var targetScreen = (Screen?)parentScreen ?? this; + + switch (client.LocalUser.State) + { + case MultiplayerUserState.Spectating: + targetScreen.Push(new MultiSpectatorScreen(room, users.Take(PlayerGrid.MAX_PLAYERS).ToArray())); + break; + + default: + targetScreen.Push(new MultiplayerPlayerLoader(() => new MultiplayerPlayer(room, new PlaylistItem(client.Room.CurrentPlaylistItem), users))); + break; + } + } + + /// + /// Responds to changes in the local user's beatmap availability to notify the server and prepare the gameplay session. + /// + private void onBeatmapAvailabilityChanged(ValueChangedEvent e) + { + if (client.Room == null || client.LocalUser == null) + return; + + client.ChangeBeatmapAvailability(e.NewValue).FireAndForget(); + + switch (e.NewValue.State) + { + case DownloadState.LocallyAvailable: + updateGameplayState(); + + // Optimistically enter spectator if the match is in progress while spectating. + if (client.LocalUser.State == MultiplayerUserState.Spectating && (client.Room.State == MultiplayerRoomState.WaitingForLoad || client.Room.State == MultiplayerRoomState.Playing)) + onLoadRequested(); + break; + + case DownloadState.NotDownloaded: + updateGameplayState(); + + if (client.LocalUser.State == MultiplayerUserState.Ready) + client.ChangeState(MultiplayerUserState.Idle); + break; + } + } + + /// + /// Updates the local user's activity to publish their presence in the room. + /// + private void updateUserActivity() + { + if (client.Room == null) + return; + + if (Activity.Value is not UserActivity.InLobby existing || existing.RoomName != client.Room.Settings.Name) + Activity.Value = new UserActivity.InLobby(client.Room); + } + + /// + /// Updates the global beatmap/ruleset/mods in preparation for a new gameplay session. + /// + private void updateGameplayState() + { + if (client.Room == null || client.LocalUser == null) + return; + + MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; + int gameplayBeatmapId = client.LocalUser.BeatmapId ?? item.BeatmapID; + int gameplayRulesetId = client.LocalUser.RulesetId ?? item.RulesetID; + + RulesetInfo ruleset = rulesets.GetRuleset(gameplayRulesetId)!; + Ruleset rulesetInstance = ruleset.CreateInstance(); + + // Update global gameplay state to correspond to the new selection. + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == gameplayBeatmapId); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + Ruleset.Value = ruleset; + Mods.Value = client.LocalUser.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray(); + + bool freemods = item.Freestyle || item.AllowedMods.Any(); + bool freestyle = item.Freestyle; + + if (freemods) + userModsSection.Show(); + else + { + userModsSection.Hide(); + userModsSelectOverlay.Hide(); + } + + if (freestyle) + { + userStyleSection.Show(); + + PlaylistItem apiItem = new PlaylistItem(item).With(beatmap: new Optional(new APIBeatmap { OnlineID = gameplayBeatmapId }), ruleset: gameplayRulesetId); + + if (!apiItem.Equals(userStyleDisplayContainer.SingleOrDefault()?.Item)) + { + userStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(apiItem, true) + { + AllowReordering = false, + AllowEditing = true, + RequestEdit = _ => showUserStyleSelect() + }; + } + } + else + userStyleSection.Hide(); + } + + /// + /// Shows the song selection screen to add or edit an item. /// /// An optional playlist item to edit. If null, a new item will be added instead. - internal void OpenSongSelection(PlaylistItem? itemToEdit = null) + public void ShowSongSelect(PlaylistItem? itemToEdit = null) { if (!this.IsCurrentScreen()) return; - this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit)); + this.Push(new MultiplayerMatchSongSelect(room, itemToEdit)); } - protected override void OpenStyleSelection() + /// + /// Shows the user mod selection. + /// + private void showUserModSelect() { - if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item) + if (!this.IsCurrentScreen()) return; - this.Push(new MultiplayerMatchFreestyleSelect(Room, item)); + userModsSelectOverlay.Show(); } - protected override Drawable CreateFooter() => new MultiplayerMatchFooter(); - - protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room); - - protected override APIMod[] GetGameplayMods() + /// + /// Shows the user style selection. + /// + private void showUserStyleSelect() { - // Using the room's reported status makes the server authoritative. - return client.LocalUser?.Mods != null ? client.LocalUser.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray() : base.GetGameplayMods(); + if (!this.IsCurrentScreen() || client.Room == null || client.LocalUser == null) + return; + + MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; + this.Push(new MultiplayerMatchFreestyleSelect(room, new PlaylistItem(item))); } - protected override RulesetInfo GetGameplayRuleset() + public override void OnEntering(ScreenTransitionEvent e) { - // Using the room's reported status makes the server authoritative. - return client.LocalUser?.RulesetId != null ? Rulesets.GetRuleset(client.LocalUser.RulesetId.Value)! : base.GetGameplayRuleset(); + base.OnEntering(e); + beginHandlingTrack(); } - protected override IBeatmapInfo GetGameplayBeatmap() + public override void OnSuspending(ScreenTransitionEvent e) { - // Using the room's reported status makes the server authoritative. - return client.LocalUser?.BeatmapId != null ? new APIBeatmap { OnlineID = client.LocalUser.BeatmapId.Value } : base.GetGameplayBeatmap(); + onLeaving(); + base.OnSuspending(e); } - [Resolved(canBeNull: true)] - private IDialogOverlay? dialogOverlay { get; set; } + public override void OnResuming(ScreenTransitionEvent e) + { + base.OnResuming(e); + beginHandlingTrack(); - private bool exitConfirmed; + // Required to update beatmap/ruleset when resuming from style selection. + updateGameplayState(); + } public override bool OnExiting(ScreenExitEvent e) { - // room has not been created yet or we're offline; exit immediately. - if (client.Room == null || !IsConnected) - return base.OnExiting(e); + if (!ensureExitConfirmed()) + return true; - if (!exitConfirmed && dialogOverlay != null) + client.LeaveRoom().FireAndForget(); + + onLeaving(); + return base.OnExiting(e); + } + + public override bool OnBackButton() + { + if (room.RoomID == null) + { + if (!ensureExitConfirmed()) + return true; + + settingsOverlay.Hide(); + return base.OnBackButton(); + } + + if (userModsSelectOverlay.State.Value == Visibility.Visible) + { + userModsSelectOverlay.Hide(); + return true; + } + + if (settingsOverlay.State.Value == Visibility.Visible) + { + settingsOverlay.Hide(); + return true; + } + + return base.OnBackButton(); + } + + private void onLeaving() + { + // Must hide this overlay because it is added to a global container. + userModsSelectOverlay.Hide(); + + endHandlingTrack(); + } + + /// + /// Handles changes in the track to keep it looping while active. + /// + private void beginHandlingTrack() + { + Beatmap.BindValueChanged(applyLoopingToTrack, true); + } + + /// + /// Stops looping the current track and stops handling further changes to the track. + /// + private void endHandlingTrack() + { + Beatmap.ValueChanged -= applyLoopingToTrack; + Beatmap.Value.Track.Looping = false; + + previewTrackManager.StopAnyPlaying(this); + } + + /// + /// Invoked on changes to the beatmap to loop the track. See: . + /// + /// The beatmap change event. + private void applyLoopingToTrack(ValueChangedEvent beatmap) + { + if (!this.IsCurrentScreen()) + return; + + beatmap.NewValue.PrepareTrackForPreview(true); + music.EnsurePlayingSomething(); + } + + /// + /// Prompts the user to discard unsaved changes to the room before exiting. + /// + /// true if the user has confirmed they want to exit. + private bool ensureExitConfirmed() + { + if (ExitConfirmed) + return true; + + if (api.State.Value != APIState.Online || !client.IsConnected.Value) + return true; + + if (dialogOverlay == null) + return true; + + bool hasUnsavedChanges = room.RoomID == null && room.Playlist.Count > 0; + + if (hasUnsavedChanges) + { + // if the dialog is already displayed, block exiting until the user explicitly makes a decision. + if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) + { + discardChangesDialog.Flash(); + return false; + } + + dialogOverlay.Push(new ConfirmDiscardChangesDialog(() => + { + ExitConfirmed = true; + settingsOverlay.Hide(); + this.Exit(); + })); + + return false; + } + + if (client.Room != null) { if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog) confirmDialog.PerformOkAction(); @@ -287,119 +813,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => { - exitConfirmed = true; - if (this.IsCurrentScreen()) - this.Exit(); + ExitConfirmed = true; + this.Exit(); })); } - return true; + return false; } - return base.OnExiting(e); - } - - protected override void PartRoom() => client.LeaveRoom(); - - private void updateBeatmapAvailability(ValueChangedEvent availability) - { - if (client.Room == null) - return; - - client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget(); - - switch (availability.NewValue.State) - { - case DownloadState.LocallyAvailable: - if (client.LocalUser?.State == MultiplayerUserState.Spectating - && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) - { - onLoadRequested(); - } - - break; - - case DownloadState.Unknown: - // Don't do anything rash in an unknown state. - break; - - default: - // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. - if (client.LocalUser?.State == MultiplayerUserState.Ready) - client.ChangeState(MultiplayerUserState.Idle); - break; - } - } - - private void onRoomUpdated() - { - // may happen if the client is kicked or otherwise removed from the room. - if (client.Room == null) - { - handleRoomLost(); - return; - } - - SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId); - - addItemButton.Alpha = localUserCanAddItem ? 1 : 0; - - Activity.Value = new UserActivity.InLobby(Room); - } - - private bool localUserCanAddItem => client.IsHost || Room.QueueMode != QueueMode.HostOnly; - - private void handleRoomLost() => Schedule(() => - { - Logger.Log($"{this} exiting due to loss of room or connection"); - - if (this.IsCurrentScreen()) - this.Exit(); - else - ValidForResume = false; - }); - - private void onLoadRequested() - { - // In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session. - // For now, we want to game to switch to the new game so need to request exiting from the play screen. - if (!ParentScreen.IsCurrentScreen()) - { - ParentScreen.MakeCurrent(); - - Schedule(onLoadRequested); - return; - } - - // The beatmap is queried asynchronously when the selected item changes. - // This is an issue with MultiSpectatorScreen which is effectively in an always "ready" state and receives LoadRequested() callbacks - // even when it is not truly ready (i.e. the beatmap hasn't been selected by the client yet). For the time being, a simple fix to this is to ignore the callback. - // Note that spectator will be entered automatically when the client is capable of doing so via beatmap availability callbacks (see: updateBeatmapAvailability()). - if (client.LocalUser?.State == MultiplayerUserState.Spectating && (SelectedItem.Value == null || Beatmap.IsDefault)) - return; - - if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable) - return; - - StartPlay(); - } - - protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) - { - Debug.Assert(client.LocalUser != null); - Debug.Assert(client.Room != null); - - int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); - MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray(); - - switch (client.LocalUser.State) - { - case MultiplayerUserState.Spectating: - return new MultiSpectatorScreen(Room, users.Take(PlayerGrid.MAX_PLAYERS).ToArray()); - - default: - return new MultiplayerPlayerLoader(() => new MultiplayerPlayer(Room, selectedItem, users)); - } + return true; } public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) @@ -407,31 +829,76 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) return; - if (!localUserCanAddItem) + if (client.Room == null || client.LocalUser == null) + return; + + if (client.Room.CanAddPlaylistItems(client.LocalUser) != true) return; // If there's only one playlist item and we are the host, assume we want to change it. Else add a new one. - PlaylistItem? itemToEdit = client.IsHost && Room.Playlist.Count == 1 ? Room.Playlist.Single() : null; + PlaylistItem? itemToEdit = client.IsHost && room.Playlist.Count == 1 ? room.Playlist.Single() : null; - OpenSongSelection(itemToEdit); + ShowSongSelect(itemToEdit); // Re-run PresentBeatmap now that we've pushed a song select that can handle it. game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID); } + // Block all input to this screen during gameplay/etc when the parent screen is no longer current. + // Normally this would be handled by ScreenStack, but we are in a child ScreenStack. + public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && (parentScreen?.IsCurrentScreen() ?? this.IsCurrentScreen()); + + // Block all input to this screen during gameplay/etc when the parent screen is no longer current. + // Normally this would be handled by ScreenStack, but we are in a child ScreenStack. + public override bool PropagateNonPositionalInputSubTree => base.PropagateNonPositionalInputSubTree && (parentScreen?.IsCurrentScreen() ?? this.IsCurrentScreen()); + + protected override BackgroundScreen CreateBackground() => new MultiplayerRoomBackgroundScreen(); + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + userModsSelectOverlayRegistration?.Dispose(); + if (client.IsNotNull()) { client.RoomUpdated -= onRoomUpdated; + client.SettingsChanged -= onSettingsChanged; + client.ItemChanged -= onItemChanged; + client.UserStyleChanged -= onUserStyleChanged; + client.UserModsChanged -= onUserModsChanged; client.LoadRequested -= onLoadRequested; } } public partial class AddItemButton : PurpleRoundedButton { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.RoomUpdated += onRoomUpdated; + onRoomUpdated(); + } + + private void onRoomUpdated() + { + if (client.Room == null || client.LocalUser == null) + return; + + Alpha = client.Room.CanAddPlaylistItems(client.LocalUser) ? 1 : 0; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + } } } } diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 16b30546de..b7b6c6f366 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -6,6 +6,7 @@ using MessagePack; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -271,6 +272,12 @@ namespace osu.Game.Users RoomName = room.Name; } + public InLobby(MultiplayerRoom room) + { + RoomID = room.RoomID; + RoomName = room.Settings.Name; + } + [SerializationConstructor] public InLobby() { } From bb1cfdca84ee3d476c7487024345eb7840a637ae Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 10:50:34 +0900 Subject: [PATCH 112/281] Remove unnecessary using --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index cff823c969..d464362fda 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -37,7 +37,6 @@ using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Users; using osu.Game.Utils; using osuTK; -using Container = osu.Framework.Graphics.Containers.Container; using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList; namespace osu.Game.Screens.OnlinePlay.Multiplayer From 2df3dfb99cc867240f757c3761115b19d8595ec1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 10:58:29 +0900 Subject: [PATCH 113/281] Remove redundant argument list --- .../ManiaFilterCriteriaTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs index 3c6046a986..24da447482 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Tests new FilterCriteria())); Assert.True(criteria.Matches( - new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), new FilterCriteria { Mods = [new ManiaModKey1()] @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Tests new FilterCriteria())); Assert.True(criteria.Matches( - new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), new FilterCriteria { Mods = [new ManiaModKey1()] @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Mania.Tests new FilterCriteria())); Assert.False(criteria.Matches( - new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), new FilterCriteria { Mods = [new ManiaModKey1()] @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Mania.Tests new FilterCriteria())); Assert.False(criteria.Matches( - new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), new FilterCriteria { Mods = [new ManiaModKey1()] @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Mania.Tests new FilterCriteria())); Assert.True(criteria.Matches( - new BeatmapInfo(new RulesetInfo() { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }), + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }), new FilterCriteria { Mods = [new ManiaModKey7()] From 32c60bfb36ae428e6fe56b077d9397c6bc57dd30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Apr 2025 19:31:16 +0900 Subject: [PATCH 114/281] Disallow adjusting scroll speed during gameplay Matches stable. Addresses https://github.com/ppy/osu/discussions/32670. --- .../UI/DrawableManiaRuleset.cs | 11 +++- .../Navigation/TestSceneScreenNavigation.cs | 58 +++++++++++++++++++ .../UI/Scrolling/DrawableScrollingRuleset.cs | 30 ++++++---- osu.Game/Screens/Play/Player.cs | 6 ++ .../PlayerSettings/BeatmapOffsetControl.cs | 25 +------- osu.Game/Screens/Play/SubmittingPlayer.cs | 18 ++++++ 6 files changed, 112 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 66400b0a55..fe3535d857 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -60,8 +60,9 @@ namespace osu.Game.Rulesets.Mania.UI private readonly BindableDouble configScrollSpeed = new BindableDouble(); private readonly Bindable mobileLayout = new Bindable(); + public double TargetTimeRange { get; protected set; } + private double currentTimeRange; - protected double TargetTimeRange; // Stores the current speed adjustment active in gameplay. private readonly Track speedAdjustmentTrack = new TrackVirtual(0); @@ -109,7 +110,13 @@ namespace osu.Game.Rulesets.Mania.UI configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed); - configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); + configScrollSpeed.BindValueChanged(speed => + { + if (!AllowScrollSpeedAdjustment) + return; + + TargetTimeRange = ComputeScrollTime(speed.NewValue); + }); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8c4fcc461c..312781ef1a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -33,6 +33,10 @@ using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Toolbar; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; @@ -394,6 +398,60 @@ namespace osu.Game.Tests.Visual.Navigation } } + [Test] + public void TestScrollSpeedAdjustDuringGameplay() + { + Player player = null; + + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("switch to mania ruleset", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.Number4); + InputManager.ReleaseKey(Key.LControl); + }); + + AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() }); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + player = Game.ScreenStack.CurrentScreen as Player; + return player?.IsLoaded == true; + }); + + AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); + checkScrollSpeed(8, 8); + + AddStep("adjust scroll speed via keyboard", () => InputManager.Key(Key.F4)); + checkScrollSpeed(9, 9); + + AddStep("seek beyond 10 seconds", () => player.ChildrenOfType().First().Seek(10500)); + AddUntilStep("wait for seek", () => player.ChildrenOfType().First().CurrentTime, () => Is.GreaterThan(10600)); + AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.F4)); + checkScrollSpeed(9, 9); + + AddStep("attempt adjust offset via config change", () => getConfigManager().SetValue(ManiaRulesetSetting.ScrollSpeed, 10.0)); + checkScrollSpeed(10, 9); + + void checkScrollSpeed(double configValue, double gameplayValue) + { + AddUntilStep($"config value is {configValue}", () => getConfigManager().Get(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue)); + AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType().Single().TargetTimeRange, + () => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue))); + } + + ManiaRulesetConfigManager getConfigManager() => ((ManiaRulesetConfigManager)Game.Dependencies.Get().GetConfigFor(new ManiaRuleset())!); + } + [Test] public void TestOffsetAdjustDuringGameplay() { diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index ba3a9bd483..f0b9876b51 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -21,6 +19,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI.Scrolling.Algorithms; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.UI.Scrolling { @@ -69,6 +68,12 @@ namespace osu.Game.Rulesets.UI.Scrolling /// protected virtual bool UserScrollSpeedAdjustment => true; + /// + /// Whether at the current point in time, whether scroll speed adjustments should be applied to gameplay. + /// This can potentially become false at some point during gameplay for game balance reasons. + /// + protected bool AllowScrollSpeedAdjustment => UserScrollSpeedAdjustment && player?.AllowCriticalSettingsAdjustment != false; + /// /// Whether beat lengths should scale relative to the most common beat length in the . /// @@ -84,7 +89,10 @@ namespace osu.Game.Rulesets.UI.Scrolling [Cached(Type = typeof(IScrollingInfo))] private readonly LocalScrollingInfo scrollingInfo; - protected DrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + [Resolved] + private Player? player { get; set; } + + protected DrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) : base(ruleset, beatmap, mods) { scrollingInfo = new LocalScrollingInfo(); @@ -195,28 +203,30 @@ namespace osu.Game.Rulesets.UI.Scrolling /// Adjusts the scroll speed of s. /// /// The amount to adjust by. Greater than 0 if the scroll speed should be increased, less than 0 if it should be decreased. - protected virtual void AdjustScrollSpeed(int amount) => this.TransformBindableTo(TimeRange, TimeRange.Value - amount * time_span_step, 200, Easing.OutQuint); + protected virtual void AdjustScrollSpeed(int amount) + { + this.TransformBindableTo(TimeRange, TimeRange.Value - amount * time_span_step, 200, Easing.OutQuint); + } public bool OnPressed(KeyBindingPressEvent e) { - if (!UserScrollSpeedAdjustment) - return false; - switch (e.Action) { case GlobalAction.IncreaseScrollSpeed: - AdjustScrollSpeed(1); + if (AllowScrollSpeedAdjustment) + AdjustScrollSpeed(1); return true; case GlobalAction.DecreaseScrollSpeed: - AdjustScrollSpeed(-1); + if (AllowScrollSpeedAdjustment) + AdjustScrollSpeed(-1); return true; } return false; } - private ScheduledDelegate scheduledScrollSpeedAdjustment; + private ScheduledDelegate? scheduledScrollSpeedAdjustment; public void OnReleased(KeyBindingReleaseEvent e) { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 612d66a896..b2e502406a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -112,6 +112,12 @@ namespace osu.Game.Screens.Play /// public IBindable ShowingOverlayComponents = new Bindable(); + /// + /// A flag which can be checked to decide whether we are in a state where settings that affect + /// game balance should be allowed to be applied at the current point in time. + /// + public virtual bool AllowCriticalSettingsAdjustment { get; } = true; + // Should match PlayerLoader for consistency. Cached here for the rare case we push a Player // without the loading screen (one such usage is the skin editor's scene library). [Cached] diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 23ccb3311b..b0b4f6cc5d 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -60,9 +60,6 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private Player? player { get; set; } - [Resolved] - private IGameplayClock? gameplayClock { get; set; } - private double lastPlayMedian; private double lastPlayBeatmapOffset; private HitEventTimingDistributionGraph? lastPlayGraph; @@ -287,27 +284,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Current.Disabled = !allow; } - private bool allowOffsetAdjust - { - get - { - // General limitations to ensure players don't do anything too weird. - // These match stable for now. - if (player is SubmittingPlayer) - { - Debug.Assert(gameplayClock != null); - - // TODO: the blocking conditions should probably display a message. - if (!player.IsBreakTime.Value && gameplayClock.CurrentTime - gameplayClock.GameplayStartTime > 10000) - return false; - - if (gameplayClock.IsPaused.Value) - return false; - } - - return true; - } - } + private bool allowOffsetAdjust => player?.AllowCriticalSettingsAdjustment != false; public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index dc3e5f08ac..7becb2b33e 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -186,6 +186,24 @@ namespace osu.Game.Screens.Play /// Whether gameplay should be immediately exited as a result. Returning false allows the gameplay session to continue. Defaults to true. protected virtual bool ShouldExitOnTokenRetrievalFailure(Exception exception) => true; + public override bool AllowCriticalSettingsAdjustment + { + get + { + // General limitations to ensure players don't do anything too weird. + // These match stable for now. + + // TODO: the blocking conditions should probably display a message. + if (!IsBreakTime.Value && GameplayClockContainer.CurrentTime - GameplayClockContainer.GameplayStartTime > 10000) + return false; + + if (GameplayClockContainer.IsPaused.Value) + return false; + + return base.AllowCriticalSettingsAdjustment; + } + } + protected override async Task PrepareScoreForResultsAsync(Score score) { await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false); From 9cdb3fe6aebf4ea994ec713c9770538499b141b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Apr 2025 19:31:28 +0900 Subject: [PATCH 115/281] Remove obsoleted `ScrollTime` setting --- .../Configuration/ManiaRulesetConfigManager.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 5242b6685c..b999a521d5 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.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; using osu.Framework.Configuration.Tracking; using osu.Game.Configuration; using osu.Game.Localisation; @@ -25,17 +24,6 @@ namespace osu.Game.Rulesets.Mania.Configuration SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait); - -#pragma warning disable CS0618 - // Although obsolete, this is still required to populate the bindable from the database in case migration is required. - SetDefault(ManiaRulesetSetting.ScrollTime, null); - - if (Get(ManiaRulesetSetting.ScrollTime) is double scrollTime) - { - SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)); - SetValue(ManiaRulesetSetting.ScrollTime, null); - } -#pragma warning restore CS0618 } public override TrackedSettings CreateTrackedSettings() => new TrackedSettings @@ -52,8 +40,6 @@ namespace osu.Game.Rulesets.Mania.Configuration public enum ManiaRulesetSetting { - [Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30 - ScrollTime, ScrollSpeed, ScrollDirection, TimingBasedNoteColouring, From 04f8fcd04f5abcd6beb747eb3040f799b870b502 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Apr 2025 19:52:34 +0900 Subject: [PATCH 116/281] Fix potential crashes due to asynchronous `BindableList` usage Band-aid fix for https://github.com/ppy/osu/issues/32671. Removes all `BindableList.BindTo` from `load()` methods (except one editor one which looks safe and is kinda hard to fix without moving drawable load to a blocking operation). --- osu.Game/Online/Chat/MessageNotifier.cs | 11 +++++--- .../Header/Components/FollowersButton.cs | 28 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 56f490cb21..49304c93a3 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -37,6 +37,9 @@ namespace osu.Game.Online.Chat [Resolved] private ChannelManager channelManager { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + [Resolved] private GameHost host { get; set; } @@ -47,19 +50,19 @@ namespace osu.Game.Online.Chat private readonly IBindableList joinedChannels = new BindableList(); [BackgroundDependencyLoader] - private void load(OsuConfigManager config, IAPIProvider api) + private void load(OsuConfigManager config) { notifyOnUsername = config.GetBindable(OsuSetting.NotifyOnUsernameMentioned); notifyOnPrivateMessage = config.GetBindable(OsuSetting.NotifyOnPrivateMessage); - - localUser.BindTo(api.LocalUser); - joinedChannels.BindTo(channelManager.JoinedChannels); } protected override void LoadComplete() { base.LoadComplete(); joinedChannels.BindCollectionChanged(channelsChanged, true); + + localUser.BindTo(api.LocalUser); + joinedChannels.BindTo(channelManager.JoinedChannels); } private void channelsChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index b93f996ec2..daf23c8ef3 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -62,8 +62,11 @@ namespace osu.Game.Overlays.Profile.Header.Components [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + [BackgroundDependencyLoader] - private void load(IAPIProvider api, INotificationOverlay? notifications) + private void load(INotificationOverlay? notifications) { localUser.BindTo(api.LocalUser); @@ -73,15 +76,6 @@ namespace osu.Game.Overlays.Profile.Header.Components updateColor(); }); - User.BindValueChanged(u => - { - followerCount = u.NewValue?.User.FollowerCount ?? 0; - updateStatus(); - }, true); - - apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged((_, _) => Schedule(updateStatus)); - Action += () => { if (User.Value == null) @@ -126,6 +120,20 @@ namespace osu.Game.Overlays.Profile.Header.Components }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + apiFriends.BindTo(api.Friends); + apiFriends.BindCollectionChanged((_, _) => Schedule(updateStatus)); + + User.BindValueChanged(u => + { + followerCount = u.NewValue?.User.FollowerCount ?? 0; + updateStatus(); + }, true); + } + protected override bool OnHover(HoverEvent e) { if (status.Value > FriendStatus.None) From 55129620b8cd10cedadc44f9799492b7d0441a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Apr 2025 12:57:55 +0200 Subject: [PATCH 117/281] Add failing test coverage --- osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 32009dc8c2..0c11c929c4 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -185,8 +185,12 @@ namespace osu.Game.Tests.Visual.Menus AddUntilStep("track changed", () => trackChangeQueue.Count == 1); AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev)); - AddUntilStep("track changed", () => + AddUntilStep("new track selected", () => trackChangeQueue.Count == 2 && !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo)); + + AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext)); + AddUntilStep("first track selected", + () => trackChangeQueue.Count == 3 && trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo)); } } } From 228d439fce31ed2dd1d6265fac97759cfc069d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Apr 2025 12:57:57 +0200 Subject: [PATCH 118/281] Fix weird behaviour when skipping back and forth with shuffle enabled Closes https://github.com/ppy/osu/issues/32590 I didn't think there would come a day where I'd unironically use a linked list, but here we are. --- osu.Game/Overlays/MusicController.cs | 115 ++++++++++++--------------- 1 file changed, 50 insertions(+), 65 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index da5388534c..e87d7fb8f2 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -73,9 +73,9 @@ namespace osu.Game.Overlays private AudioFilter audioDuckFilter = null!; private readonly Bindable randomSelectAlgorithm = new Bindable(); - private readonly List> previousRandomSets = new List>(); - private int randomHistoryDirection; - private int lastRandomTrackDirection; + + private readonly LinkedList> randomHistory = new LinkedList>(); + private LinkedListNode>? currentRandomHistoryPosition; [BackgroundDependencyLoader] private void load(AudioManager audio, OsuConfigManager configManager) @@ -371,81 +371,66 @@ namespace osu.Game.Overlays private Live? getNextRandom(int direction, bool allowProtectedTracks) { - try + Live result; + + var possibleSets = getBeatmapSets(allowProtectedTracks).ToList(); + + if (possibleSets.Count == 0) + return null; + + // if there is only one possible set left, play it, even if it is the same as the current track. + // looping is preferable over playing nothing. + if (possibleSets.Count == 1) + return possibleSets.Single(); + + // now that we actually know there is a choice, do not allow the current track to be played again. + possibleSets.RemoveAll(s => s.Value.Equals(current?.BeatmapSetInfo)); + + if (currentRandomHistoryPosition != null) { - Live result; + if (direction < 0 && currentRandomHistoryPosition.Previous != null) + return (currentRandomHistoryPosition = currentRandomHistoryPosition.Previous).Value; - var possibleSets = getBeatmapSets(allowProtectedTracks).ToList(); + if (direction > 0 && currentRandomHistoryPosition.Next != null) + return (currentRandomHistoryPosition = currentRandomHistoryPosition.Next).Value; + } - if (possibleSets.Count == 0) - return null; + // if the early-return above didn't cover it, it means that we have no history to fall back on + // and need to actually choose something random. - // if there is only - // one possible set left, play it, even if it is the same as the current track. - // looping is preferable over playing nothing. - if (possibleSets.Count == 1) - return possibleSets.Single(); + Debug.Assert(randomHistory.Count == 0 + || (currentRandomHistoryPosition == randomHistory.First && direction < 0) + || (currentRandomHistoryPosition == randomHistory.Last && direction > 0)); - // now that we actually know there is a choice, do not allow the current track to be played again. - possibleSets.RemoveAll(s => s.Value.Equals(current?.BeatmapSetInfo)); + switch (randomSelectAlgorithm.Value) + { + case RandomSelectAlgorithm.Random: + result = possibleSets[RNG.Next(possibleSets.Count)]; + break; - // condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero. - // if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back, - // or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward. - // in both cases, it means that we have a history of previous random selections that we can rewind. - if (randomHistoryDirection * direction < 0) - { - Debug.Assert(Math.Abs(randomHistoryDirection) == previousRandomSets.Count); + case RandomSelectAlgorithm.RandomPermutation: + var notYetPlayedSets = possibleSets.Except(randomHistory).ToList(); - // if the user has been shuffling backwards and now going forwards (or vice versa), - // the topmost item from history needs to be discarded because it's the *current* track. - if (direction * lastRandomTrackDirection < 0) + if (notYetPlayedSets.Count == 0) { - previousRandomSets.RemoveAt(previousRandomSets.Count - 1); - randomHistoryDirection += direction; + possibleSets.RemoveAll(s => s.Value.Equals(current?.BeatmapSetInfo)); + notYetPlayedSets = possibleSets; + randomHistory.Clear(); } - if (previousRandomSets.Count > 0) - { - result = previousRandomSets[^1]; - previousRandomSets.RemoveAt(previousRandomSets.Count - 1); - return result; - } - } + result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Count)]; - // if the early-return above didn't cover it, it means that we have no history to fall back on - // and need to actually choose something random. - switch (randomSelectAlgorithm.Value) - { - case RandomSelectAlgorithm.Random: - result = possibleSets[RNG.Next(possibleSets.Count)]; - break; + if (randomHistory.Count == 0 || (currentRandomHistoryPosition == randomHistory.Last && direction > 0)) + currentRandomHistoryPosition = randomHistory.AddLast(result); + else if (currentRandomHistoryPosition == randomHistory.First && direction < 0) + currentRandomHistoryPosition = randomHistory.AddFirst(result); + break; - case RandomSelectAlgorithm.RandomPermutation: - var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToList(); - - if (notYetPlayedSets.Count == 0) - { - notYetPlayedSets = possibleSets; - previousRandomSets.Clear(); - randomHistoryDirection = 0; - } - - result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Count)]; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm"); - } - - previousRandomSets.Add(result); - return result; - } - finally - { - randomHistoryDirection += direction; - lastRandomTrackDirection = direction; + default: + throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm"); } + + return result; } private void restartTrack() From f5b849b4f36a27d06ed42efff921504749d49e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Apr 2025 11:17:04 +0200 Subject: [PATCH 119/281] Remove filtering & rearranging controls from now playing overlay --- .../UserInterface/TestScenePlaylistOverlay.cs | 114 ------------ osu.Game/Overlays/Music/FilterControl.cs | 76 -------- osu.Game/Overlays/Music/FilterCriteria.cs | 25 --- osu.Game/Overlays/Music/Playlist.cs | 48 +---- osu.Game/Overlays/Music/PlaylistItem.cs | 164 ++++++++---------- osu.Game/Overlays/Music/PlaylistOverlay.cs | 51 ++---- 6 files changed, 91 insertions(+), 387 deletions(-) delete mode 100644 osu.Game/Overlays/Music/FilterControl.cs delete mode 100644 osu.Game/Overlays/Music/FilterCriteria.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index c723988d6a..0bda4f3d35 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -8,15 +8,12 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; -using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Collections; using osu.Game.Database; using osu.Game.Overlays.Music; using osu.Game.Rulesets; using osu.Game.Tests.Resources; using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { @@ -67,116 +64,5 @@ namespace osu.Game.Tests.Visual.UserInterface // Ensure all the initial imports are present before running any tests. Realm.Run(r => r.Refresh()); }); - - [Test] - public void TestRearrangeItems() - { - AddUntilStep("wait for load complete", () => - { - return this - .ChildrenOfType() - .Count(i => i.ChildrenOfType().First().DelayedLoadCompleted) > 6; - }); - - AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any()); - - PlaylistItem firstItem = null!; - - AddStep("hold 1st item handle", () => - { - firstItem = this.ChildrenOfType().First(); - var handle = firstItem.ChildrenOfType().First(); - - InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre); - InputManager.PressButton(MouseButton.Left); - }); - - AddStep("drag to 5th", () => - { - var item = this.ChildrenOfType().ElementAt(4); - InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft); - }); - - AddAssert("first is moved", () => playlistOverlay.ChildrenOfType().Single().Items.ElementAt(4).Value.Equals(firstItem.Model.Value)); - - AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left)); - } - - [Test] - public void TestFiltering() - { - AddStep("set filter to \"10\"", () => - { - var filterControl = playlistOverlay.ChildrenOfType().Single(); - filterControl.Search.Current.Value = "10"; - }); - - AddAssert("results filtered correctly", - () => playlistOverlay.ChildrenOfType() - .Where(item => item.MatchingFilter) - .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10")))); - - AddStep("Import new non-matching beatmap", () => - { - var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(1); - testBeatmapSetInfo.Beatmaps.Single().Metadata.Title = "no guid"; - beatmapManager.Import(testBeatmapSetInfo); - }); - - AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); - - AddAssert("results filtered correctly", - () => playlistOverlay.ChildrenOfType() - .Where(item => item.MatchingFilter) - .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10")))); - } - - [Test] - public void TestCollectionFiltering() - { - NowPlayingCollectionDropdown collectionDropdown() => playlistOverlay.ChildrenOfType().Single(); - - AddStep("Add collection", () => - { - Realm.Write(r => - { - r.RemoveAll(); - r.Add(new BeatmapCollection("wang")); - }); - }); - - AddUntilStep("wait for dropdown to have new collection", () => collectionDropdown().Items.Count() == 2); - - AddStep("Filter to collection", () => - { - collectionDropdown().Current.Value = collectionDropdown().Items.Last(); - }); - - AddUntilStep("No items present", () => !playlistOverlay.ChildrenOfType().Any(i => i.MatchingFilter)); - - AddStep("Import new non-matching beatmap", () => - { - beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(1)); - }); - - AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); - - AddUntilStep("No items matching", () => !playlistOverlay.ChildrenOfType().Any(i => i.MatchingFilter)); - - BeatmapSetInfo collectionAddedBeatmapSet = null!; - - AddStep("Import new matching beatmap", () => - { - collectionAddedBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1); - - beatmapManager.Import(collectionAddedBeatmapSet); - Realm.Write(r => r.All().First().BeatmapMD5Hashes.Add(collectionAddedBeatmapSet.Beatmaps.First().MD5Hash)); - }); - - AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); - - AddUntilStep("Only matching item", - () => playlistOverlay.ChildrenOfType().Where(i => i.MatchingFilter).Select(i => i.Model.ID), () => Is.EquivalentTo(new[] { collectionAddedBeatmapSet.ID })); - } } } diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs deleted file mode 100644 index a61702645a..0000000000 --- a/osu.Game/Overlays/Music/FilterControl.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osuTK; -using System; -using osu.Framework.Allocation; - -namespace osu.Game.Overlays.Music -{ - public partial class FilterControl : Container - { - public Action FilterChanged; - - public readonly FilterTextBox Search; - private readonly NowPlayingCollectionDropdown collectionDropdown; - - public FilterControl() - { - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 10f), - Children = new Drawable[] - { - Search = new FilterTextBox - { - RelativeSizeAxes = Axes.X, - Height = 40, - }, - collectionDropdown = new NowPlayingCollectionDropdown { RelativeSizeAxes = Axes.X } - }, - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Search.Current.BindValueChanged(_ => updateCriteria()); - collectionDropdown.Current.BindValueChanged(_ => updateCriteria(), true); - } - - private void updateCriteria() => FilterChanged?.Invoke(createCriteria()); - - private FilterCriteria createCriteria() => new FilterCriteria - { - SearchText = Search.Current.Value, - Collection = collectionDropdown.Current.Value?.Collection - }; - - public partial class FilterTextBox : BasicSearchTextBox - { - protected override bool AllowCommit => true; - - [BackgroundDependencyLoader] - private void load() - { - Masking = true; - CornerRadius = 5; - - BackgroundUnfocused = OsuColour.Gray(0.06f); - BackgroundFocused = OsuColour.Gray(0.12f); - } - } - } -} diff --git a/osu.Game/Overlays/Music/FilterCriteria.cs b/osu.Game/Overlays/Music/FilterCriteria.cs deleted file mode 100644 index ad491be845..0000000000 --- a/osu.Game/Overlays/Music/FilterCriteria.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using JetBrains.Annotations; -using osu.Game.Collections; -using osu.Game.Database; - -namespace osu.Game.Overlays.Music -{ - public class FilterCriteria - { - /// - /// The search text. - /// - public string SearchText; - - /// - /// The collection to filter beatmaps from. - /// - [CanBeNull] - public Live Collection; - } -} diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index ab51ca7e1d..d7f35e6131 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -1,67 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Linq; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Containers; -using osuTK; namespace osu.Game.Overlays.Music { - public partial class Playlist : OsuRearrangeableListContainer> + public partial class Playlist : VirtualisedListContainer, PlaylistItem> { - public Action>? RequestSelection; - - public readonly Bindable> SelectedSet = new Bindable>(); - - private FilterCriteria currentCriteria = new FilterCriteria(); - public new MarginPadding Padding { get => base.Padding; set => base.Padding = value; } - protected override void OnItemsChanged() + public Playlist() + : base(20, 50) { - base.OnItemsChanged(); - Filter(currentCriteria); } - public void Filter(FilterCriteria criteria) - { - var items = (SearchContainer>>)ListContainer; - - string[]? currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray()); - - foreach (var item in items.OfType()) - { - item.InSelectedCollection = currentCollectionHashes == null || item.Model.Value.Beatmaps.Select(b => b.MD5Hash).Any(currentCollectionHashes.Contains); - } - - items.SearchTerm = criteria.SearchText; - currentCriteria = criteria; - } - - public Live? FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); - - protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) => - new PlaylistItem(item) - { - SelectedSet = { BindTarget = SelectedSet }, - RequestSelection = set => RequestSelection?.Invoke(set) - }; - - protected override FillFlowContainer>> CreateListFillFlowContainer() => new SearchContainer>> - { - Spacing = new Vector2(0, 3), - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, - }; + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); } } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 01b0472172..750742aebd 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -1,15 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -20,118 +18,106 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Music { - public partial class PlaylistItem : OsuRearrangeableListItem>, IFilterable + public partial class PlaylistItem : PoolableDrawable, IHasCurrentValue> { - public readonly Bindable> SelectedSet = new Bindable>(); + public Bindable> Current + { + get => current.Current; + set => current.Current = value; + } - public Action> RequestSelection; + private readonly BindableWithCurrent> current = new BindableWithCurrent>(); - private TextFlowContainer text; - private ITextPart titlePart; + private readonly Bindable?> selectedSet = new Bindable?>(); + private Action>? requestSelection; + + private TextFlowContainer text = null!; + private ITextPart? titlePart; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - public PlaylistItem(Live item) - : base(item) + [Resolved] + private PlaylistOverlay playlistOverlay { get; set; } = null!; + + public PlaylistItem() { - Padding = new MarginPadding { Left = 5 }; + Padding = new MarginPadding { Horizontal = 10 }; } [BackgroundDependencyLoader] - private void load() + private void load(PlaylistOverlay playlistOverlay) { - HandleColour = colours.Gray5; + RelativeSizeAxes = Axes.X; + Height = 20; + + InternalChild = text = new OsuTextFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }; + + selectedSet.BindTo(playlistOverlay.SelectedSet); + requestSelection = playlistOverlay.ItemSelected; } protected override void LoadComplete() { base.LoadComplete(); - - Model.PerformRead(m => - { - var metadata = m.Metadata; - - var title = new RomanisableString(metadata.TitleUnicode, metadata.Title); - var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - - titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); - titlePart.DrawablePartsRecreated += _ => updateSelectionState(SelectedSet.Value, applyImmediately: true); - - text.AddText(@" "); // to separate the title from the artist. - text.AddText(artist, sprite => - { - sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); - sprite.Colour = colours.Gray9; - }); - - SelectedSet.BindValueChanged(set => updateSelectionState(set.NewValue)); - updateSelectionState(SelectedSet.Value, applyImmediately: true); - }); + Current.BindValueChanged(_ => onItemChanged(), true); + selectedSet.BindValueChanged(updateSelectionState, true); } - private bool selected; - - private void updateSelectionState(Live selectedSet, bool applyImmediately = false) + private void onItemChanged() => Current.Value.PerformRead(m => { - bool wasSelected = selected; - selected = selectedSet?.Equals(Model) == true; + var metadata = m.Metadata; - // Immediate updates should forcibly set correct state regardless of previous state. - // This ensures that the initial state is correctly applied. - if (wasSelected == selected && !applyImmediately) + var title = new RomanisableString(metadata.TitleUnicode, metadata.Title); + var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + + text.Clear(); + + titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); + titlePart.DrawablePartsRecreated += _ => + { + selectedSet.TriggerChange(); + FinishTransforms(true); + }; + + text.AddText(@" "); // to separate the title from the artist. + text.AddText(artist, sprite => + { + sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + sprite.Colour = colours.Gray9; + }); + + selectedSet.TriggerChange(); + FinishTransforms(true); + }); + + private bool? selected; + + private void updateSelectionState(ValueChangedEvent?> selected) + { + bool? wasSelected = this.selected; + this.selected = selected.NewValue?.Equals(Current.Value) == true; + + if (wasSelected == this.selected) return; - foreach (Drawable s in titlePart.Drawables) - s.FadeColour(selected ? colours.Yellow : Color4.White, applyImmediately ? 0 : FADE_DURATION); + if (titlePart != null) + { + foreach (Drawable s in titlePart.Drawables) + s.FadeColour(this.selected == true ? colours.Yellow : Color4.White, 100); + } } - protected override Drawable CreateContent() => new DelayedLoadWrapper(text = new OsuTextFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }); - protected override bool OnClick(ClickEvent e) { - RequestSelection?.Invoke(Model); + requestSelection?.Invoke(Current.Value); return true; } - - private bool inSelectedCollection = true; - - public bool InSelectedCollection - { - get => inSelectedCollection; - set - { - if (inSelectedCollection == value) - return; - - inSelectedCollection = value; - updateFilter(); - } - } - - public IEnumerable FilterTerms => Model.PerformRead(m => m.Metadata.GetSearchableTerms()).Select(s => (LocalisableString)s).ToArray(); - - private bool matchingFilter = true; - - public bool MatchingFilter - { - get => matchingFilter && inSelectedCollection; - set - { - if (matchingFilter == value) - return; - - matchingFilter = value; - updateFilter(); - } - } - - private void updateFilter() => this.FadeTo(MatchingFilter ? 1 : 0, 200); - - public bool FilteringActive { get; set; } } } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index b49c794aa3..99ae88701a 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Allocation; @@ -21,8 +19,11 @@ using Realms; namespace osu.Game.Overlays.Music { + [Cached] public partial class PlaylistOverlay : VisibilityContainer { + public Bindable?> SelectedSet = new Bindable?>(); + private const float transition_duration = 600; public const float PLAYLIST_HEIGHT = 510; @@ -31,15 +32,14 @@ namespace osu.Game.Overlays.Music private readonly Bindable beatmap = new Bindable(); [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private RealmAccess realm { get; set; } + private RealmAccess realm { get; set; } = null!; - private IDisposable beatmapSubscription; + private IDisposable? beatmapSubscription; - private FilterControl filter; - private Playlist list; + private Playlist list = null!; [BackgroundDependencyLoader] private void load(OsuColour colours, Bindable beatmap) @@ -69,33 +69,11 @@ namespace osu.Game.Overlays.Music list = new Playlist { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 95, Bottom = 10, Right = 10 }, - RequestSelection = itemSelected - }, - filter = new FilterControl - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - FilterChanged = criteria => list.Filter(criteria), - Padding = new MarginPadding(10), + Padding = new MarginPadding { Vertical = 10, Right = 10 }, }, }, }, }; - - filter.Search.OnCommit += (_, _) => - { - list.FirstVisibleSet?.PerformRead(set => - { - BeatmapInfo toSelect = set.Beatmaps.FirstOrDefault(); - - if (toSelect != null) - { - beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect); - beatmap.Value.Track.Restart(); - } - }); - }; } protected override void LoadComplete() @@ -104,11 +82,11 @@ namespace osu.Game.Overlays.Music beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending && !s.Protected), beatmapsChanged); - list.Items.BindTo(beatmapSets); - beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); + list.RowData.BindTo(beatmapSets); + beatmap.BindValueChanged(working => SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); } - private void beatmapsChanged(IRealmCollection sender, ChangeSet changes) + private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) { if (changes == null) { @@ -127,22 +105,17 @@ namespace osu.Game.Overlays.Music protected override void PopIn() { - filter.Search.HoldFocus = true; - Schedule(() => filter.Search.TakeFocus()); - this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlag(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint); this.FadeIn(transition_duration, Easing.OutQuint); } protected override void PopOut() { - filter.Search.HoldFocus = false; - this.ResizeTo(new Vector2(1, 0), transition_duration, Easing.OutQuint); this.FadeOut(transition_duration); } - private void itemSelected(Live beatmapSet) + public void ItemSelected(Live beatmapSet) { beatmapSet.PerformRead(set => { From 52d71d7f6edb5d749e8596271fdc17ce01504b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 4 Apr 2025 12:06:55 +0200 Subject: [PATCH 120/281] Make overflowing playlist items scroll --- osu.Game/Overlays/Music/PlaylistItem.cs | 52 +++--- osu.Game/Overlays/NowPlayingOverlay.cs | 158 +++++------------- .../Overlays/OverflowScrollingContainer.cs | 87 ++++++++++ 3 files changed, 150 insertions(+), 147 deletions(-) create mode 100644 osu.Game/Overlays/OverflowScrollingContainer.cs diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 750742aebd..055d72fbd4 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -31,32 +31,22 @@ namespace osu.Game.Overlays.Music private readonly Bindable?> selectedSet = new Bindable?>(); private Action>? requestSelection; - private TextFlowContainer text = null!; - private ITextPart? titlePart; + private OverflowScrollingContainer text = null!; [Resolved] private OsuColour colours { get; set; } = null!; - [Resolved] - private PlaylistOverlay playlistOverlay { get; set; } = null!; - - public PlaylistItem() - { - Padding = new MarginPadding { Horizontal = 10 }; - } - [BackgroundDependencyLoader] private void load(PlaylistOverlay playlistOverlay) { RelativeSizeAxes = Axes.X; Height = 20; - InternalChild = text = new OsuTextFlowContainer + InternalChild = text = new OverflowScrollingContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.X, }; selectedSet.BindTo(playlistOverlay.SelectedSet); @@ -77,22 +67,26 @@ namespace osu.Game.Overlays.Music var title = new RomanisableString(metadata.TitleUnicode, metadata.Title); var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - text.Clear(); - - titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); - titlePart.DrawablePartsRecreated += _ => + text.CreateContent.Value = () => { - selectedSet.TriggerChange(); - FinishTransforms(true); + var flow = new OsuTextFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }; + + flow.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)); + flow.AddText(@" "); // to separate the title from the artist. + flow.AddText(artist, sprite => + { + sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + sprite.Colour = colours.Gray9; + }); + return flow; }; - text.AddText(@" "); // to separate the title from the artist. - text.AddText(artist, sprite => - { - sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); - sprite.Colour = colours.Gray9; - }); - selectedSet.TriggerChange(); FinishTransforms(true); }); @@ -107,11 +101,7 @@ namespace osu.Game.Overlays.Music if (wasSelected == this.selected) return; - if (titlePart != null) - { - foreach (Drawable s in titlePart.Drawables) - s.FadeColour(this.selected == true ? colours.Yellow : Color4.White, 100); - } + text.FadeColour(this.selected == true ? colours.Yellow : Color4.White, 100); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index f4da9a92dc..7e34ce2103 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -5,7 +5,6 @@ using System; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -50,7 +49,7 @@ namespace osu.Game.Overlays private MusicIconButton shuffleButton = null!; private IconButton playlistButton = null!; - private ScrollingTextContainer title = null!, artist = null!; + private OverflowScrollingContainer title = null!, artist = null!; private PlaylistOverlay? playlist; @@ -72,6 +71,9 @@ namespace osu.Game.Overlays private Bindable allowTrackControl = null!; private readonly BindableBool shuffle = new BindableBool(true); + private static readonly FontUsage title_font = OsuFont.GetFont(size: 25, italics: true); + private static readonly FontUsage artist_font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold, italics: true); + public NowPlayingOverlay() { Width = player_width; @@ -105,23 +107,41 @@ namespace osu.Game.Overlays Children = new[] { background = Empty(), - title = new ScrollingTextContainer + title = new OverflowScrollingContainer { Origin = Anchor.BottomCentre, Anchor = Anchor.TopCentre, Position = new Vector2(0, 40), - Font = OsuFont.GetFont(size: 25, italics: true), Colour = Color4.White, - Text = @"Nothing to play", + CreateContent = + { + Value = () => new OsuSpriteText + { + Font = title_font, + Text = @"Nothing to play", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }, + NonOverflowingContentAnchor = Anchor.Centre, }, - artist = new ScrollingTextContainer + artist = new OverflowScrollingContainer { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, Position = new Vector2(0, 45), - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold, italics: true), Colour = Color4.White, - Text = @"Nothing to play", + CreateContent = + { + Value = () => new OsuSpriteText + { + Font = artist_font, + Text = @"Nothing to play", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }, + NonOverflowingContentAnchor = Anchor.Centre, }, new Container { @@ -318,8 +338,20 @@ namespace osu.Game.Overlays { BeatmapMetadata metadata = beatmap.Metadata; - title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); - artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); + title.CreateContent.Value = () => new OsuSpriteText + { + Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), + Font = title_font, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + artist.CreateContent.Value = () => new OsuSpriteText + { + Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), + Font = artist_font, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; backgroundLoadCancellation?.Cancel(); @@ -484,111 +516,5 @@ namespace osu.Game.Overlays base.OnHoverLost(e); } } - - private partial class ScrollingTextContainer : CompositeDrawable - { - private const float initial_move_delay = 1000; - private const float pixels_per_second = 50; - - private OsuSpriteText mainSpriteText = null!; - private OsuSpriteText fillerSpriteText = null!; - - private Bindable showUnicode = null!; - - [Resolved] - private FrameworkConfigManager frameworkConfig { get; set; } = null!; - - private LocalisableString text; - - public LocalisableString Text - { - get => text; - set - { - text = value; - - if (IsLoaded) - updateText(); - } - } - - private FontUsage font = OsuFont.Default; - - public FontUsage Font - { - get => font; - set - { - font = value; - - if (IsLoaded) - updateFontAndText(); - } - } - - public ScrollingTextContainer() - { - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new[] - { - mainSpriteText = new OsuSpriteText { Padding = new MarginPadding { Horizontal = margin } }, - fillerSpriteText = new OsuSpriteText { Padding = new MarginPadding { Horizontal = margin }, Alpha = 0 }, - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - showUnicode = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode); - showUnicode.BindValueChanged(_ => updateText()); - - updateFontAndText(); - } - - private void updateFontAndText() - { - mainSpriteText.Font = font; - fillerSpriteText.Font = font; - - updateText(); - } - - private void updateText() - { - mainSpriteText.Text = text; - fillerSpriteText.Alpha = 0; - - ClearTransforms(); - X = 0; - - float textOverflowWidth = mainSpriteText.Width - player_width; - - // apply half margin of tolerance on both sides before the text scrolls - if (textOverflowWidth > margin) - { - fillerSpriteText.Alpha = 1; - fillerSpriteText.Text = text; - - float initialX = (textOverflowWidth + mainSpriteText.Width) / 2; - float targetX = (textOverflowWidth - mainSpriteText.Width) / 2; - - this.MoveToX(initialX) - .Delay(initial_move_delay) - .MoveToX(targetX, mainSpriteText.Width * 1000 / pixels_per_second) - .Loop(); - } - } - } } } diff --git a/osu.Game/Overlays/OverflowScrollingContainer.cs b/osu.Game/Overlays/OverflowScrollingContainer.cs new file mode 100644 index 0000000000..a789916185 --- /dev/null +++ b/osu.Game/Overlays/OverflowScrollingContainer.cs @@ -0,0 +1,87 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Overlays +{ + public partial class OverflowScrollingContainer : CompositeDrawable + { + public Anchor NonOverflowingContentAnchor { get; init; } = Anchor.TopLeft; + + public Bindable> CreateContent = new Bindable>(); + + private const float initial_move_delay = 1000; + private const float pixels_per_second = 50; + private const float padding = 15; + + private Drawable mainContent = null!; + private Drawable fillerContent = null!; + private FillFlowContainer flow = null!; + + public OverflowScrollingContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = flow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(padding), + Padding = new MarginPadding { Horizontal = padding }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + CreateContent.BindValueChanged(_ => + { + flow.Clear(); + flow.Add(mainContent = CreateContent.Value.Invoke()); + flow.Add(fillerContent = CreateContent.Value.Invoke().With(d => d.Alpha = 0)); + ScheduleAfterChildren(updateText); + }, true); + } + + private void updateText() + { + fillerContent.Alpha = 0; + + flow.ClearTransforms(); + flow.X = 0; + + float overflowWidth = mainContent.DrawWidth + padding - DrawWidth; + + if (overflowWidth > 0) + { + fillerContent.Alpha = 1; + + float targetX = mainContent.DrawWidth + padding; + + flow.MoveToX(0) + .Delay(initial_move_delay) + .MoveToX(-targetX, targetX * 1000 / pixels_per_second) + .Loop(); + flow.Anchor = Anchor.TopLeft; + flow.Origin = Anchor.TopLeft; + } + else + { + flow.Anchor = NonOverflowingContentAnchor; + flow.Origin = NonOverflowingContentAnchor; + } + } + } +} From 40e792b55817c5601f94a9819fa687c07483f2a0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 21:04:46 +0900 Subject: [PATCH 121/281] Allow viewing results of historical multiplayer items --- .../Match/Playlist/MultiplayerHistoryList.cs | 1 + .../Match/Playlist/MultiplayerPlaylist.cs | 8 ++++++- .../Multiplayer/MultiplayerMatchSubScreen.cs | 17 +++++++++++++- .../Playlists/PlaylistsRoomSubScreen.cs | 22 ++++++++++++++----- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs index d18bb011f0..14b1aa38be 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerHistoryList.cs @@ -18,6 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist public MultiplayerHistoryList() { ShowItemOwners = true; + AllowShowingResults = true; } protected override FillFlowContainer> CreateListFillFlowContainer() => new HistoryFillFlowContainer diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index fba3acc32a..5af0fed48f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -21,10 +21,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist public readonly Bindable DisplayMode = new Bindable(); /// - /// Invoked when an item requests to be edited. + /// Invoked when the user requests to edit an item. /// public Action? RequestEdit; + /// + /// Invoked when the user requests to view the results for an item. + /// + public Action? RequestResults; + [Resolved] private MultiplayerClient client { get; set; } = null!; @@ -62,6 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { RelativeSizeAxes = Axes.Both, Alpha = 0, + RequestResults = item => RequestResults?.Invoke(item) } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index d464362fda..b22851052b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -34,6 +34,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Users; using osu.Game.Utils; using osuTK; @@ -272,7 +273,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new MultiplayerPlaylist { RelativeSizeAxes = Axes.Both, - RequestEdit = ShowSongSelect + RequestEdit = ShowSongSelect, + RequestResults = showResults } }, new Drawable[] @@ -670,6 +672,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer this.Push(new MultiplayerMatchFreestyleSelect(room, new PlaylistItem(item))); } + /// + /// Shows the results screen for a playlist item. + /// + private void showResults(PlaylistItem item) + { + if (!this.IsCurrentScreen() || client.Room == null || client.LocalUser == null) + return; + + // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). + var targetScreen = (Screen?)parentScreen ?? this; + targetScreen.Push(new PlaylistItemUserBestResultsScreen(client.Room.RoomID, item, client.LocalUser.UserID)); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 305a81bdbe..91723fbec3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -250,12 +250,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists SelectedItem = { BindTarget = SelectedItem }, AllowSelection = true, AllowShowingResults = true, - RequestResults = item => - { - Debug.Assert(room.RoomID != null); - parentScreen?.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, - api.LocalUser.Value.Id)); - } + RequestResults = showResults } }, new Drawable[] @@ -689,6 +684,21 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }); } + /// + /// Shows the results screen for a playlist item. + /// + private void showResults(PlaylistItem item) + { + if (!this.IsCurrentScreen()) + return; + + Debug.Assert(room.RoomID != null); + + // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes). + var targetScreen = (Screen?)parentScreen ?? this; + targetScreen.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, api.LocalUser.Value.OnlineID)); + } + /// /// May be invoked by the owner of the room to permanently close the room ahead of its intended end date. /// From 2aaadc1a900d8d5518b7b3e4c699e1c7b70cf8f9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 21:11:32 +0900 Subject: [PATCH 122/281] Remove dimming from expired panels --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 1e1e79d256..9e585d584d 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -128,9 +128,6 @@ namespace osu.Game.Screens.OnlinePlay Item = item; valid.BindTo(item.Valid); - - if (item.Expired) - Colour = OsuColour.Gray(0.5f); } [BackgroundDependencyLoader] From 3a231debee500acf7ab3c1a80e72071f78d45cbb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 21:17:04 +0900 Subject: [PATCH 123/281] Don't select items when viewing results --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index c9d8365852..423c956d1c 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -165,7 +165,8 @@ namespace osu.Game.Screens.OnlinePlay d.RequestDeletion = i => RequestDeletion?.Invoke(i); d.RequestResults = i => { - SelectedItem.Value = i; + if (AllowSelection) + SelectedItem.Value = i; RequestResults?.Invoke(i); }; d.RequestEdit = i => RequestEdit?.Invoke(i); From abfda9cbcd716f611d3da6b85d2568f761d53c59 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 22:26:26 +0900 Subject: [PATCH 124/281] Fix thread-race leading to `OnScreenDisplay` crash --- osu.Game/Overlays/OnScreenDisplay.cs | 32 +++++--------- osu.Game/Rulesets/UI/DrawableRuleset.cs | 58 ++++++++++++------------- 2 files changed, 37 insertions(+), 53 deletions(-) diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index 4f2dba7b2c..672505ee54 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Graphics; @@ -56,7 +57,8 @@ namespace osu.Game.Overlays /// The to be tracked. /// If is null. /// If is already being tracked from the same . - public void BeginTracking(object source, ITrackableConfigManager configManager) + /// An object representing the registration, that may be disposed to stop tracking the . + public IDisposable BeginTracking(object source, ITrackableConfigManager configManager) { ArgumentNullException.ThrowIfNull(configManager); @@ -65,32 +67,18 @@ namespace osu.Game.Overlays var trackedSettings = configManager.CreateTrackedSettings(); if (trackedSettings == null) - return; + return new InvokeOnDisposal(() => { }); configManager.LoadInto(trackedSettings); trackedSettings.SettingChanged += displayTrackedSettingChange; - trackedConfigManagers.Add((source, configManager), trackedSettings); - } - /// - /// Unregisters a from having its settings tracked by this . - /// - /// The object that registered the to be tracked. - /// The that is being tracked. - /// If is null. - /// If is not being tracked from the same . - public void StopTracking(object source, ITrackableConfigManager configManager) - { - ArgumentNullException.ThrowIfNull(configManager); - - if (!trackedConfigManagers.TryGetValue((source, configManager), out var existing)) - return; - - existing.Unload(); - existing.SettingChanged -= displayTrackedSettingChange; - - trackedConfigManagers.Remove((source, configManager)); + return new InvokeOnDisposal(() => + { + trackedSettings.Unload(); + trackedSettings.SettingChanged -= displayTrackedSettingChange; + trackedConfigManagers.Remove((source, configManager)); + }); } /// diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 13d4b67132..74de5849ab 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -54,7 +54,12 @@ namespace osu.Game.Rulesets.UI /// /// The key conversion input manager for this DrawableRuleset. /// - protected PassThroughInputManager KeyBindingInputManager; + protected PassThroughInputManager KeyBindingInputManager { get; } + + /// + /// This configuration for this DrawableRuleset. + /// + protected IRulesetConfigManager Config { get; private set; } public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0; @@ -77,8 +82,26 @@ namespace osu.Game.Rulesets.UI public override IFrameStableClock FrameStableClock => frameStabilityContainer; + public override IEnumerable Objects => Beatmap.HitObjects; + + /// + /// The beatmap. + /// + [Cached(typeof(IBeatmap))] + public readonly Beatmap Beatmap; + + [Cached(typeof(IReadOnlyList))] + public sealed override IReadOnlyList Mods { get; } + + [Resolved(CanBeNull = true)] + private OnScreenDisplay onScreenDisplay { get; set; } + private readonly PlayfieldAdjustmentContainer playfieldAdjustmentContainer; + private IDisposable configTracker; + private FrameStabilityContainer frameStabilityContainer; + private DrawableRulesetDependencies dependencies; + private bool allowBackwardsSeeks; public override bool AllowBackwardsSeeks @@ -105,25 +128,6 @@ namespace osu.Game.Rulesets.UI } } - /// - /// The beatmap. - /// - [Cached(typeof(IBeatmap))] - public readonly Beatmap Beatmap; - - public override IEnumerable Objects => Beatmap.HitObjects; - - protected IRulesetConfigManager Config { get; private set; } - - [Cached(typeof(IReadOnlyList))] - public sealed override IReadOnlyList Mods { get; } - - private FrameStabilityContainer frameStabilityContainer; - - private OnScreenDisplay onScreenDisplay; - - private DrawableRulesetDependencies dependencies; - /// /// Creates a ruleset visualisation for the provided ruleset and beatmap. /// @@ -156,6 +160,8 @@ namespace osu.Game.Rulesets.UI { base.LoadComplete(); + configTracker = onScreenDisplay?.BeginTracking(this, Config); + IsPaused.ValueChanged += paused => { if (HasReplayLoaded.Value) @@ -168,13 +174,7 @@ namespace osu.Game.Rulesets.UI protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { dependencies = new DrawableRulesetDependencies(Ruleset, base.CreateChildDependencies(parent)); - Config = dependencies.RulesetConfigManager; - - onScreenDisplay = dependencies.Get(); - if (Config != null) - onScreenDisplay?.BeginTracking(this, Config); - return dependencies; } @@ -404,11 +404,7 @@ namespace osu.Game.Rulesets.UI { base.Dispose(isDisposing); - if (Config != null) - { - onScreenDisplay?.StopTracking(this, Config); - Config = null; - } + configTracker?.Dispose(); // Dispose the components created by this dependency container. dependencies?.Dispose(); From 6d5188889666303390496712b4bd1979b5bc0ed1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 22:30:49 +0900 Subject: [PATCH 125/281] Prevent future misuse --- osu.Game/Overlays/OnScreenDisplay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index 672505ee54..c2ffb8ba6c 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; @@ -60,6 +62,8 @@ namespace osu.Game.Overlays /// An object representing the registration, that may be disposed to stop tracking the . public IDisposable BeginTracking(object source, ITrackableConfigManager configManager) { + Debug.Assert(ThreadSafety.IsUpdateThread); + ArgumentNullException.ThrowIfNull(configManager); if (trackedConfigManagers.ContainsKey((source, configManager))) From 319e3db14e390e8bf71d3c9520331946b3263947 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 4 Apr 2025 23:17:16 +0900 Subject: [PATCH 126/281] Resolve test failures --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 74de5849ab..97c4ee45af 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -160,7 +160,8 @@ namespace osu.Game.Rulesets.UI { base.LoadComplete(); - configTracker = onScreenDisplay?.BeginTracking(this, Config); + if (Config != null) + configTracker = onScreenDisplay?.BeginTracking(this, Config); IsPaused.ValueChanged += paused => { From a039bfe1d10a04fdae17dd44f67d0a92e9b76655 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 5 Apr 2025 22:43:33 +0900 Subject: [PATCH 127/281] Rename class to avoid miconceptions --- .../{OverflowScrollingContainer.cs => MarqueeContainer.cs} | 4 ++-- osu.Game/Overlays/Music/PlaylistItem.cs | 4 ++-- osu.Game/Overlays/NowPlayingOverlay.cs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Overlays/{OverflowScrollingContainer.cs => MarqueeContainer.cs} (95%) diff --git a/osu.Game/Overlays/OverflowScrollingContainer.cs b/osu.Game/Overlays/MarqueeContainer.cs similarity index 95% rename from osu.Game/Overlays/OverflowScrollingContainer.cs rename to osu.Game/Overlays/MarqueeContainer.cs index a789916185..1a7e3b3443 100644 --- a/osu.Game/Overlays/OverflowScrollingContainer.cs +++ b/osu.Game/Overlays/MarqueeContainer.cs @@ -10,7 +10,7 @@ using osuTK; namespace osu.Game.Overlays { - public partial class OverflowScrollingContainer : CompositeDrawable + public partial class MarqueeContainer : CompositeDrawable { public Anchor NonOverflowingContentAnchor { get; init; } = Anchor.TopLeft; @@ -24,7 +24,7 @@ namespace osu.Game.Overlays private Drawable fillerContent = null!; private FillFlowContainer flow = null!; - public OverflowScrollingContainer() + public MarqueeContainer() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 055d72fbd4..56c3f00bd1 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Music private readonly Bindable?> selectedSet = new Bindable?>(); private Action>? requestSelection; - private OverflowScrollingContainer text = null!; + private MarqueeContainer text = null!; [Resolved] private OsuColour colours { get; set; } = null!; @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Music RelativeSizeAxes = Axes.X; Height = 20; - InternalChild = text = new OverflowScrollingContainer + InternalChild = text = new MarqueeContainer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 7e34ce2103..24dffdc066 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays private MusicIconButton shuffleButton = null!; private IconButton playlistButton = null!; - private OverflowScrollingContainer title = null!, artist = null!; + private MarqueeContainer title = null!, artist = null!; private PlaylistOverlay? playlist; @@ -107,7 +107,7 @@ namespace osu.Game.Overlays Children = new[] { background = Empty(), - title = new OverflowScrollingContainer + title = new MarqueeContainer { Origin = Anchor.BottomCentre, Anchor = Anchor.TopCentre, @@ -125,7 +125,7 @@ namespace osu.Game.Overlays }, NonOverflowingContentAnchor = Anchor.Centre, }, - artist = new OverflowScrollingContainer + artist = new MarqueeContainer { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, From 3287cc2e5551b5d4c0cb55210bd4b75a3b108944 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 5 Apr 2025 23:34:43 +0900 Subject: [PATCH 128/281] Remove unused local variable in test --- .../Visual/UserInterface/TestScenePlaylistOverlay.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 0bda4f3d35..2672854e19 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -21,8 +21,6 @@ namespace osu.Game.Tests.Visual.UserInterface { protected override bool UseFreshStoragePerRun => true; - private PlaylistOverlay playlistOverlay = null!; - private BeatmapManager beatmapManager = null!; private const int item_count = 20; @@ -45,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(300, 500), - Child = playlistOverlay = new PlaylistOverlay + Child = new PlaylistOverlay { Anchor = Anchor.Centre, Origin = Anchor.Centre, From b8360a19dd96a18075821c056f4c6723b4d3379e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 09:18:16 +0200 Subject: [PATCH 129/281] Only scroll overflowing playlist items if they've been hovered --- osu.Game/Overlays/MarqueeContainer.cs | 54 +++++++++++++++++-------- osu.Game/Overlays/Music/PlaylistItem.cs | 13 ++++++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/MarqueeContainer.cs b/osu.Game/Overlays/MarqueeContainer.cs index 1a7e3b3443..8540f5edd9 100644 --- a/osu.Game/Overlays/MarqueeContainer.cs +++ b/osu.Game/Overlays/MarqueeContainer.cs @@ -6,12 +6,23 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Threading; using osuTK; namespace osu.Game.Overlays { public partial class MarqueeContainer : CompositeDrawable { + /// + /// Whether the marquee should be allowed to scroll the content if it overflows. + /// Note that upon changing the value of this, any existing scrolls will be allowed to complete their current loop if they're mid-scroll. + /// + public Bindable AllowScrolling { get; } = new BindableBool(true); + + /// + /// The to anchor the content to if it does not overflow. + /// public Anchor NonOverflowingContentAnchor { get; init; } = Anchor.TopLeft; public Bindable> CreateContent = new Bindable>(); @@ -37,6 +48,8 @@ namespace osu.Game.Overlays { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Anchor = NonOverflowingContentAnchor, + Origin = NonOverflowingContentAnchor, Spacing = new Vector2(padding), Padding = new MarginPadding { Horizontal = padding }, }; @@ -46,41 +59,50 @@ namespace osu.Game.Overlays { base.LoadComplete(); + AllowScrolling.BindValueChanged(_ => ScheduleAfterChildren(() => updateScrolling(instant: false))); CreateContent.BindValueChanged(_ => { flow.Clear(); flow.Add(mainContent = CreateContent.Value.Invoke()); flow.Add(fillerContent = CreateContent.Value.Invoke().With(d => d.Alpha = 0)); - ScheduleAfterChildren(updateText); + ScheduleAfterChildren(() => updateScrolling(instant: true)); }, true); } - private void updateText() - { - fillerContent.Alpha = 0; + private TransformSequence? scrollSequence; + private ScheduledDelegate? scheduledScrollCancel; - flow.ClearTransforms(); - flow.X = 0; + private void updateScrolling(bool instant) + { + scheduledScrollCancel?.Cancel(); + scheduledScrollCancel = null; float overflowWidth = mainContent.DrawWidth + padding - DrawWidth; - if (overflowWidth > 0) + if (overflowWidth > 0 && AllowScrolling.Value) { fillerContent.Alpha = 1; + flow.Anchor = Anchor.TopLeft; + flow.Origin = Anchor.TopLeft; float targetX = mainContent.DrawWidth + padding; - flow.MoveToX(0) - .Delay(initial_move_delay) - .MoveToX(-targetX, targetX * 1000 / pixels_per_second) - .Loop(); - flow.Anchor = Anchor.TopLeft; - flow.Origin = Anchor.TopLeft; + scrollSequence ??= flow.MoveToX(0) + .Delay(initial_move_delay) + .MoveToX(-targetX, targetX * 1000 / pixels_per_second) + .Loop(); } - else + else if (scrollSequence != null) { - flow.Anchor = NonOverflowingContentAnchor; - flow.Origin = NonOverflowingContentAnchor; + scheduledScrollCancel = Scheduler.AddDelayed(() => + { + fillerContent.Alpha = 0; + flow.ClearTransforms(); + flow.X = 0; + flow.Anchor = NonOverflowingContentAnchor; + flow.Origin = NonOverflowingContentAnchor; + scrollSequence = null; + }, instant ? 0 : flow.LatestTransformEndTime - Time.Current); } } } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 56c3f00bd1..8503a078e1 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -47,6 +47,7 @@ namespace osu.Game.Overlays.Music Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, + AllowScrolling = { Value = false } }; selectedSet.BindTo(playlistOverlay.SelectedSet); @@ -109,5 +110,17 @@ namespace osu.Game.Overlays.Music requestSelection?.Invoke(Current.Value); return true; } + + protected override bool OnHover(HoverEvent e) + { + text.AllowScrolling.Value = true; + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + text.AllowScrolling.Value = false; + base.OnHoverLost(e); + } } } From 2d619a3692556eecdb370886cec9791459d7dba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 09:36:53 +0200 Subject: [PATCH 130/281] Apply review suggestions --- osu.Game/Overlays/MusicController.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index e87d7fb8f2..8bb88fc8e9 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -389,19 +389,21 @@ namespace osu.Game.Overlays if (currentRandomHistoryPosition != null) { if (direction < 0 && currentRandomHistoryPosition.Previous != null) - return (currentRandomHistoryPosition = currentRandomHistoryPosition.Previous).Value; + { + currentRandomHistoryPosition = currentRandomHistoryPosition.Previous; + return currentRandomHistoryPosition.Value; + } if (direction > 0 && currentRandomHistoryPosition.Next != null) - return (currentRandomHistoryPosition = currentRandomHistoryPosition.Next).Value; + { + currentRandomHistoryPosition = currentRandomHistoryPosition.Next; + return currentRandomHistoryPosition.Value; + } } // if the early-return above didn't cover it, it means that we have no history to fall back on // and need to actually choose something random. - Debug.Assert(randomHistory.Count == 0 - || (currentRandomHistoryPosition == randomHistory.First && direction < 0) - || (currentRandomHistoryPosition == randomHistory.Last && direction > 0)); - switch (randomSelectAlgorithm.Value) { case RandomSelectAlgorithm.Random: @@ -420,9 +422,16 @@ namespace osu.Game.Overlays result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Count)]; - if (randomHistory.Count == 0 || (currentRandomHistoryPosition == randomHistory.Last && direction > 0)) + Debug.Assert(randomHistory.Count == 0 + || (currentRandomHistoryPosition == randomHistory.First && direction < 0) + || (currentRandomHistoryPosition == randomHistory.Last && direction > 0)); + + // notably, this depends solely on `direction` specifically, because when there are less than 2 items in `randomHistory`, + // we have `randomHistory.First == randomHistory.Last` (either `null` if no items, or the single item). + // the assert above should make that safe to depend on. + if (direction > 0) currentRandomHistoryPosition = randomHistory.AddLast(result); - else if (currentRandomHistoryPosition == randomHistory.First && direction < 0) + else if (direction < 0) currentRandomHistoryPosition = randomHistory.AddFirst(result); break; From 3738d75202a41bac2d1fa3ad19dcd00823763e9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Apr 2025 19:37:16 +0900 Subject: [PATCH 131/281] Avoid saving a state when clearly not dirty --- osu.Game/Screens/Edit/Editor.cs | 4 ++++ osu.Game/Screens/Edit/Setup/MetadataSection.cs | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index f56380a34d..572c4ce283 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -573,6 +573,9 @@ namespace osu.Game.Screens.Edit return true; } + [CanBeNull] + internal event Action Saved; + /// /// Saves the currently edited beatmap. /// @@ -601,6 +604,7 @@ namespace osu.Game.Screens.Edit isNewBeatmap = false; updateLastSavedHash(); onScreenDisplay?.Display(new BeatmapEditorToast(ToastStrings.BeatmapSaved, editorBeatmap.BeatmapInfo.GetDisplayTitle())); + Saved?.Invoke(); return true; } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index ef9657f32e..323cdcfc3d 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -25,8 +25,13 @@ namespace osu.Game.Screens.Edit.Setup private FormTextBox sourceTextBox = null!; private FormTextBox tagsTextBox = null!; + private bool dirty = false; + public override LocalisableString Title => EditorSetupStrings.MetadataHeader; + [Resolved] + private Editor editor { get; set; } + [BackgroundDependencyLoader] private void load(SetupScreen? setupScreen) { @@ -73,11 +78,13 @@ namespace osu.Game.Screens.Edit.Setup item.Current.BindValueChanged(_ => applyMetadata()); item.OnCommit += (_, newText) => { - if (newText) + if (newText && dirty) Beatmap.SaveState(); }; } + editor.Saved += () => dirty = false; + updateReadOnlyState(); } @@ -124,6 +131,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.DifficultyName = difficultyTextBox.Current.Value; Beatmap.Metadata.Source = sourceTextBox.Current.Value; Beatmap.Metadata.Tags = tagsTextBox.Current.Value; + + dirty = true; } private partial class FormRomanisedTextBox : FormTextBox From 0d9cff487b2cc6f1bb2ffb99bd83c3e3869a6a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 12:47:24 +0200 Subject: [PATCH 132/281] Terminate scrolling immediately on unhovering --- osu.Game/Overlays/MarqueeContainer.cs | 40 ++++++++++----------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/osu.Game/Overlays/MarqueeContainer.cs b/osu.Game/Overlays/MarqueeContainer.cs index 8540f5edd9..69ac5f7d06 100644 --- a/osu.Game/Overlays/MarqueeContainer.cs +++ b/osu.Game/Overlays/MarqueeContainer.cs @@ -6,8 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Threading; using osuTK; namespace osu.Game.Overlays @@ -16,7 +14,7 @@ namespace osu.Game.Overlays { /// /// Whether the marquee should be allowed to scroll the content if it overflows. - /// Note that upon changing the value of this, any existing scrolls will be allowed to complete their current loop if they're mid-scroll. + /// Note that upon changing the value of this, any existing scrolls will be terminated instantly. /// public Bindable AllowScrolling { get; } = new BindableBool(true); @@ -59,24 +57,18 @@ namespace osu.Game.Overlays { base.LoadComplete(); - AllowScrolling.BindValueChanged(_ => ScheduleAfterChildren(() => updateScrolling(instant: false))); + AllowScrolling.BindValueChanged(_ => ScheduleAfterChildren(updateScrolling)); CreateContent.BindValueChanged(_ => { flow.Clear(); flow.Add(mainContent = CreateContent.Value.Invoke()); flow.Add(fillerContent = CreateContent.Value.Invoke().With(d => d.Alpha = 0)); - ScheduleAfterChildren(() => updateScrolling(instant: true)); + ScheduleAfterChildren(updateScrolling); }, true); } - private TransformSequence? scrollSequence; - private ScheduledDelegate? scheduledScrollCancel; - - private void updateScrolling(bool instant) + private void updateScrolling() { - scheduledScrollCancel?.Cancel(); - scheduledScrollCancel = null; - float overflowWidth = mainContent.DrawWidth + padding - DrawWidth; if (overflowWidth > 0 && AllowScrolling.Value) @@ -87,22 +79,18 @@ namespace osu.Game.Overlays float targetX = mainContent.DrawWidth + padding; - scrollSequence ??= flow.MoveToX(0) - .Delay(initial_move_delay) - .MoveToX(-targetX, targetX * 1000 / pixels_per_second) - .Loop(); + flow.MoveToX(0) + .Delay(initial_move_delay) + .MoveToX(-targetX, targetX * 1000 / pixels_per_second) + .Loop(); } - else if (scrollSequence != null) + else { - scheduledScrollCancel = Scheduler.AddDelayed(() => - { - fillerContent.Alpha = 0; - flow.ClearTransforms(); - flow.X = 0; - flow.Anchor = NonOverflowingContentAnchor; - flow.Origin = NonOverflowingContentAnchor; - scrollSequence = null; - }, instant ? 0 : flow.LatestTransformEndTime - Time.Current); + fillerContent.Alpha = 0; + flow.ClearTransforms(); + flow.X = 0; + flow.Anchor = NonOverflowingContentAnchor; + flow.Origin = NonOverflowingContentAnchor; } } } From 3852cca19aa873ed9ba69dc7bef88580d8573440 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Apr 2025 20:00:53 +0900 Subject: [PATCH 133/281] Disable new "duplciated statements" inspection --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 5cac0024b7..b8a455e2f1 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -88,6 +88,7 @@ HINT DO_NOT_SHOW WARNING + HINT DO_NOT_SHOW WARNING WARNING From aa8ebf989b902f25bc8cd0787d1d2774d7dcd705 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Apr 2025 20:10:27 +0900 Subject: [PATCH 134/281] Add back removed hash check --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index d464362fda..f1736903df 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -599,7 +599,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // Update global gameplay state to correspond to the new selection. // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == gameplayBeatmapId); + var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmapId); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = ruleset; Mods.Value = client.LocalUser.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray(); From 2dc38b5f094746f2933852f812458863719e5307 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Apr 2025 20:12:43 +0900 Subject: [PATCH 135/281] Disable incorrect cancellation token inspection --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 5c840a8357..f4d8e8518e 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -68,6 +68,7 @@ namespace osu.Game.Rulesets.Difficulty cancellationToken = timedCancellationSource.Token; cancellationToken.ThrowIfCancellationRequested(); + // ReSharper disable once PossiblyMistakenUseOfCancellationToken preProcess(mods, cancellationToken); var skills = CreateSkills(Beatmap, playableMods, clockRate); @@ -109,6 +110,7 @@ namespace osu.Game.Rulesets.Difficulty cancellationToken = timedCancellationSource.Token; cancellationToken.ThrowIfCancellationRequested(); + // ReSharper disable once PossiblyMistakenUseOfCancellationToken preProcess(mods, cancellationToken); var attribs = new List(); From 43ee43c9e28c57f7f774c24d46fb3642ce045824 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Apr 2025 20:36:02 +0900 Subject: [PATCH 136/281] Switch to using a popover again --- .../Visual/Ranking/TestSceneUserTagControl.cs | 29 +- osu.Game/Screens/Ranking/UserTagControl.cs | 311 +++++++++--------- 2 files changed, 174 insertions(+), 166 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index 958eacfd56..c546c9727c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -2,12 +2,15 @@ // 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.Cursor; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Ranking; @@ -16,6 +19,9 @@ namespace osu.Game.Tests.Visual.Ranking { public partial class TestSceneUserTagControl : OsuTestScene { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; [SetUpSteps] @@ -35,8 +41,15 @@ namespace osu.Game.Tests.Visual.Ranking [ new APITag { Id = 0, Name = "uncategorised tag", Description = "This probably isn't real but could be and should be handled.", }, new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", }, - new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", }, - new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", }, + new APITag + { + Id = 2, Name = "style/clean", + Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", + }, + new APITag + { + Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", + }, new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", }, new APITag { Id = 5, Name = "style/mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, }, ] @@ -84,11 +97,15 @@ namespace osu.Game.Tests.Visual.Ranking private void recreateControl() { - Child = new UserTagControl(Beatmap.Value.BeatmapInfo) + Child = new PopoverContainer { - Width = 700, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = new UserTagControl(Beatmap.Value.BeatmapInfo) + { + Width = 700, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } }; } } diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index bfc54e8423..5692c844ed 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -9,13 +9,15 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; +using osu.Framework.Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Bindings; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -26,10 +28,12 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Screens.Ranking.Statistics; using osuTK; @@ -55,6 +59,8 @@ namespace osu.Game.Screens.Ranking private APIRequest? requestInFlight; + private AddNewTagUserTag addNewTagUserTag = null!; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -99,14 +105,14 @@ namespace osu.Game.Screens.Ranking AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, Spacing = new Vector2(4), + Child = addNewTagUserTag = new AddNewTagUserTag + { + AvailableTags = { BindTarget = relevantTagsById }, + OnTagSelected = toggleVote, + }, }, }, }, - new TagList - { - AvailableTags = { BindTarget = relevantTagsById }, - OnSelected = toggleVote, - } } } }, @@ -196,7 +202,7 @@ namespace osu.Game.Screens.Ranking { var tag = (UserTag)e.OldItems[i]!; tag.VoteCount.ValueChanged -= voteCountChanged; - tagFlow.Remove(oldItems[e.OldStartingIndex + i], true); + tagFlow.Remove(oldItems[1 + e.OldStartingIndex + i], true); } break; @@ -205,6 +211,7 @@ namespace osu.Game.Screens.Ranking case NotifyCollectionChangedAction.Reset: { tagFlow.Clear(); + tagFlow.Add(addNewTagUserTag); break; } } @@ -279,7 +286,12 @@ namespace osu.Game.Screens.Ranking .Select((tag, index) => new KeyValuePair(tag, index))); foreach (var drawableTag in tagFlow) - tagFlow.SetLayoutPosition(drawableTag, sortedTags[drawableTag.UserTag]); + { + if (drawableTag == addNewTagUserTag) + tagFlow.SetLayoutPosition(drawableTag, float.MinValue); + else + tagFlow.SetLayoutPosition(drawableTag, sortedTags[drawableTag.UserTag]); + } layout.Validate(); } @@ -287,10 +299,6 @@ namespace osu.Game.Screens.Ranking protected override bool OnClick(ClickEvent e) => true; - public void OnReleased(KeyBindingReleaseEvent e) - { - } - private partial class DrawableUserTag : OsuAnimatedButton { public readonly UserTag UserTag; @@ -301,18 +309,22 @@ namespace osu.Game.Screens.Ranking private readonly BindableBool voted = new BindableBool(); private readonly Bindable confirmed = new BindableBool(); - private Box mainBackground = null!; + protected Box MainBackground { get; private set; } = null!; private Box voteBackground = null!; - private OsuSpriteText tagCategoryText = null!; - private OsuSpriteText tagNameText = null!; - private OsuSpriteText voteCountText = null!; + + protected OsuSpriteText TagCategoryText { get; private set; } = null!; + protected OsuSpriteText TagNameText { get; private set; } = null!; + protected OsuSpriteText VoteCountText { get; private set; } = null!; + + private readonly bool showVoteCount; [Resolved] private OsuColour colours { get; set; } = null!; - public DrawableUserTag(UserTag userTag) + public DrawableUserTag(UserTag userTag, bool showVoteCount = true) { UserTag = userTag; + this.showVoteCount = showVoteCount; voteCount.BindTo(userTag.VoteCount); voted.BindTo(userTag.Voted); @@ -334,7 +346,7 @@ namespace osu.Game.Screens.Ranking }; Content.AddRange(new Drawable[] { - mainBackground = new Box + MainBackground = new Box { RelativeSizeAxes = Axes.Both, Depth = float.MaxValue, @@ -343,9 +355,9 @@ namespace osu.Game.Screens.Ranking { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Children = new Drawable[] + Children = new[] { - tagCategoryText = new OsuSpriteText + TagCategoryText = new OsuSpriteText { Alpha = UserTag.GroupName != null ? 0.6f : 0, Text = UserTag.GroupName ?? default(LocalisableString), @@ -355,8 +367,7 @@ namespace osu.Game.Screens.Ranking }, new Container { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Children = new Drawable[] @@ -367,33 +378,35 @@ namespace osu.Game.Screens.Ranking Alpha = 0.1f, Blending = BlendingParameters.Additive, }, - tagNameText = new OsuSpriteText + TagNameText = new OsuSpriteText { Text = UserTag.DisplayName, Font = OsuFont.Default.With(weight: FontWeight.SemiBold), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Horizontal = 6 } - }, - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Children = new Drawable[] - { - voteBackground = new Box - { - RelativeSizeAxes = Axes.Both, - }, - voteCountText = new OsuSpriteText - { Margin = new MarginPadding { Horizontal = 6, Vertical = 3, }, }, } - } + }, + showVoteCount + ? new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + voteBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + VoteCountText = new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 6, Vertical = 3, }, + }, + } + } + : Empty(), } } }); @@ -407,52 +420,90 @@ namespace osu.Game.Screens.Ranking const double transition_duration = 300; - voteCount.BindValueChanged(_ => + if (showVoteCount) { - voteCountText.Text = voteCount.Value.ToLocalisableString(); - confirmed.Value = voteCount.Value >= 10; - }, true); - voted.BindValueChanged(v => - { - if (v.NewValue) + voteCount.BindValueChanged(_ => { - voteBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint); - voteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); - } - else + VoteCountText.Text = voteCount.Value.ToLocalisableString(); + confirmed.Value = voteCount.Value >= 10; + }, true); + voted.BindValueChanged(v => { - voteBackground.FadeColour(colours.Gray2, transition_duration, Easing.OutQuint); - voteCountText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); - } - }, true); - confirmed.BindValueChanged(c => - { - if (c.NewValue) + if (v.NewValue) + { + voteBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint); + VoteCountText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + } + else + { + voteBackground.FadeColour(colours.Gray2, transition_duration, Easing.OutQuint); + VoteCountText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + } + }, true); + + confirmed.BindValueChanged(c => { - mainBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint); - tagCategoryText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); - tagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); - FadeEdgeEffectTo(0.3f, transition_duration, Easing.OutQuint); - } - else - { - mainBackground.FadeColour(colours.Gray6, transition_duration, Easing.OutQuint); - tagCategoryText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); - tagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); - FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint); - } - }, true); + if (c.NewValue) + { + MainBackground.FadeColour(colours.Lime2, transition_duration, Easing.OutQuint); + TagCategoryText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + TagNameText.FadeColour(Colour4.Black, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0.3f, transition_duration, Easing.OutQuint); + } + else + { + MainBackground.FadeColour(colours.Gray6, transition_duration, Easing.OutQuint); + TagCategoryText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + TagNameText.FadeColour(Colour4.White, transition_duration, Easing.OutQuint); + FadeEdgeEffectTo(0f, transition_duration, Easing.OutQuint); + } + }, true); + } + FinishTransforms(true); Action = () => OnSelected?.Invoke(UserTag); } } - private partial class TagList : CompositeDrawable, IKeyBindingHandler + private partial class AddNewTagUserTag : DrawableUserTag, IHasPopover + { + public BindableDictionary AvailableTags { get; } = new BindableDictionary(); + + public Action? OnTagSelected { get; set; } + + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + + public AddNewTagUserTag() + : base(new UserTag(new APITag { Name = "+/add" }), false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AvailableTags.BindCollectionChanged((_, _) => Enabled.Value = AvailableTags.Count > 0, true); + Action = this.ShowPopover; + + MainBackground.FadeColour(overlayColourProvider.Background2); + TagCategoryText.FadeColour(overlayColourProvider.Colour0); + TagNameText.FadeColour(overlayColourProvider.Colour0); + FadeEdgeEffectTo(0); + } + + public Popover GetPopover() => new AddTagsPopover + { + AvailableTags = { BindTarget = AvailableTags }, + OnSelected = OnTagSelected, + }; + } + + private partial class AddTagsPopover : OsuPopover { private SearchTextBox searchBox = null!; private SearchContainer searchContainer = null!; - private Container content = null!; public BindableDictionary AvailableTags { get; } = new BindableDictionary(); @@ -460,79 +511,42 @@ namespace osu.Game.Screens.Ranking private CancellationTokenSource? loadCancellationTokenSource; - private readonly BindableBool expanded = new BindableBool(); - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - Margin = new MarginPadding { Left = 30 }; - InternalChildren = new Drawable[] + AllowableAnchors = new[] + { + Anchor.TopCentre, + }; + + Children = new Drawable[] { - new OsuClickableContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopLeft, - Origin = Anchor.TopRight, - X = 10, - Masking = true, - CornerRadius = 5, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray5, - }, - new SpriteIcon - { - Size = new Vector2(16), - Icon = FontAwesome.Solid.Plus, - Margin = new MarginPadding(10), - } - }, - Action = expanded.Toggle, - }, new Container { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, + Size = new Vector2(400, 300), Children = new Drawable[] { - new Box + searchBox = new SearchTextBox { - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray5, + HoldFocus = true, + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, }, - content = new Container + new OsuScrollContainer { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10) { Top = 12 }, - Children = new Drawable[] + RelativeSizeAxes = Axes.X, + Y = 40, + Height = 260, + ScrollbarOverlapsContent = false, + Child = searchContainer = new SearchContainer { - searchBox = new SearchTextBox - { - HoldFocus = true, - RelativeSizeAxes = Axes.X, - Depth = float.MinValue, - Y = -2, // hacky compensation for masking issues - }, - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 42, }, - ScrollbarOverlapsContent = false, - Child = searchContainer = new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 5, Bottom = 10 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - } - } - }, - }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 5, Bottom = 10 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + } + } }, }, }; @@ -554,25 +568,6 @@ namespace osu.Game.Screens.Ranking }, loadCancellationTokenSource.Token); }, true); searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true); - expanded.BindValueChanged(_ => - { - const float transition_duration = 250; - - if (expanded.Value) - { - this.ResizeWidthTo(400, transition_duration, Easing.OutQuint); - content.FadeIn(250, Easing.OutQuint); - RelativeSizeAxes = Axes.None; - this.ResizeHeightTo(300, transition_duration, Easing.OutQuint); - } - else - { - this.ResizeWidthTo(10, transition_duration, Easing.OutQuint); - content.FadeOut(250, Easing.OutQuint); - RelativeSizeAxes = Axes.Y; - this.ResizeHeightTo(1, transition_duration, Easing.OutQuint); - } - }, true); } private IEnumerable createItems(IEnumerable tags) @@ -590,7 +585,7 @@ namespace osu.Game.Screens.Ranking } } - public bool OnPressed(KeyBindingPressEvent e) + public override bool OnPressed(KeyBindingPressEvent e) { if (e.Action == GlobalAction.Select && !e.Repeat) { @@ -601,10 +596,6 @@ namespace osu.Game.Screens.Ranking return false; } - public void OnReleased(KeyBindingReleaseEvent e) - { - } - private void attemptSelect() { var visibleItems = searchContainer.ChildrenOfType().Where(d => d.IsPresent).ToArray(); From f8d063a39424cebd9f63bd4742cf3f6c61d74b0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Apr 2025 20:00:36 +0900 Subject: [PATCH 137/281] Move loading inline to individual tags --- osu.Game/Screens/Ranking/UserTag.cs | 1 + osu.Game/Screens/Ranking/UserTagControl.cs | 40 ++++++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTag.cs b/osu.Game/Screens/Ranking/UserTag.cs index 983f585931..9a93df91b5 100644 --- a/osu.Game/Screens/Ranking/UserTag.cs +++ b/osu.Game/Screens/Ranking/UserTag.cs @@ -16,6 +16,7 @@ namespace osu.Game.Screens.Ranking public BindableInt VoteCount { get; } = new BindableInt(); public BindableBool Voted { get; } = new BindableBool(); + public BindableBool Updating { get; } = new BindableBool(); public UserTag(APITag tag) { diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 5692c844ed..e88ffd507e 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -48,7 +48,6 @@ namespace osu.Game.Screens.Ranking private readonly Cached layout = new Cached(); private FillFlowContainer tagFlow = null!; - private LoadingLayer loadingLayer = null!; private BindableList displayedTags { get; } = new BindableList(); @@ -116,11 +115,6 @@ namespace osu.Game.Screens.Ranking } } }, - loadingLayer = new LoadingLayer(dimBackground: true) - { - RelativeSizeAxes = Axes.Both, - State = { Value = Visibility.Visible } - }, }; apiTags = sessionStatics.GetBindable(Static.AllBeatmapTags); @@ -163,6 +157,7 @@ namespace osu.Game.Screens.Ranking if (relevantTagsById.TryGetValue(topTag.TagId, out var tag)) { tag.VoteCount.Value = topTag.VoteCount; + tag.Updating.Value = false; displayedTags.Add(tag); } } @@ -170,10 +165,11 @@ namespace osu.Game.Screens.Ranking foreach (long ownTagId in apiBeatmap.Value.OwnTagIds ?? []) { if (relevantTagsById.TryGetValue(ownTagId, out var tag)) + { tag.Voted.Value = true; + tag.Updating.Value = false; + } } - - loadingLayer.Hide(); } private void displayTags(object? sender, NotifyCollectionChangedEventArgs e) @@ -222,7 +218,7 @@ namespace osu.Game.Screens.Ranking if (requestInFlight != null) return; - loadingLayer.Show(); + tag.Updating.Value = true; APIRequest request; @@ -253,12 +249,12 @@ namespace osu.Game.Screens.Ranking request.Success += () => { - loadingLayer.Hide(); + tag.Updating.Value = false; requestInFlight = null; }; request.Failure += _ => { - loadingLayer.Hide(); + tag.Updating.Value = false; requestInFlight = null; }; api.Queue(requestInFlight = request); @@ -308,6 +304,7 @@ namespace osu.Game.Screens.Ranking private readonly Bindable voteCount = new Bindable(); private readonly BindableBool voted = new BindableBool(); private readonly Bindable confirmed = new BindableBool(); + private readonly BindableBool updating = new BindableBool(); protected Box MainBackground { get; private set; } = null!; private Box voteBackground = null!; @@ -318,6 +315,8 @@ namespace osu.Game.Screens.Ranking private readonly bool showVoteCount; + private LoadingLayer loadingLayer = null!; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -326,6 +325,7 @@ namespace osu.Game.Screens.Ranking UserTag = userTag; this.showVoteCount = showVoteCount; voteCount.BindTo(userTag.VoteCount); + updating.BindTo(userTag.Updating); voted.BindTo(userTag.Voted); AutoSizeAxes = Axes.Both; @@ -408,7 +408,8 @@ namespace osu.Game.Screens.Ranking } : Empty(), } - } + }, + loadingLayer = new LoadingLayer(dimBackground: true), }); TooltipText = UserTag.Description; @@ -420,6 +421,8 @@ namespace osu.Game.Screens.Ranking const double transition_duration = 300; + updating.BindValueChanged(u => loadingLayer.State.Value = u.NewValue ? Visibility.Visible : Visibility.Hidden); + if (showVoteCount) { voteCount.BindValueChanged(_ => @@ -636,6 +639,9 @@ namespace osu.Game.Screens.Ranking private SpriteIcon votedIcon = null!; private readonly Bindable voted = new Bindable(); + private readonly BindableBool updating = new BindableBool(); + + private LoadingLayer loadingLayer = null!; public DrawableAddableTag(UserTag tag) { @@ -645,6 +651,9 @@ namespace osu.Game.Screens.Ranking AutoSizeAxes = Axes.Y; ScaleOnMouseDown = 0.95f; + + voted.BindTo(Tag.Voted); + updating.BindTo(Tag.Updating); } [Resolved] @@ -705,10 +714,9 @@ namespace osu.Game.Screens.Ranking Text = Tag.Description, } } - } + }, + loadingLayer = new LoadingLayer(dimBackground: true), }); - - voted.BindTo(Tag.Voted); } public IEnumerable FilterTerms => [Tag.FullName, Tag.Description]; @@ -726,6 +734,8 @@ namespace osu.Game.Screens.Ranking votedIcon.FadeColour(voted.Value ? Colour4.Black : Colour4.White, 250, Easing.OutQuint); }, true); FinishTransforms(true); + + updating.BindValueChanged(u => loadingLayer.State.Value = u.NewValue ? Visibility.Visible : Visibility.Hidden); } } } From b874eea0c5482d5aa421abd3296115a6b6f9582e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Apr 2025 20:28:45 +0900 Subject: [PATCH 138/281] Allow multiple in-flight requests --- osu.Game/Screens/Ranking/UserTagControl.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index e88ffd507e..797d66b5c5 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -56,8 +56,6 @@ namespace osu.Game.Screens.Ranking private readonly Bindable apiBeatmap = new Bindable(); - private APIRequest? requestInFlight; - private AddNewTagUserTag addNewTagUserTag = null!; [Resolved] @@ -215,7 +213,7 @@ namespace osu.Game.Screens.Ranking private void toggleVote(UserTag tag) { - if (requestInFlight != null) + if (tag.Updating.Value) return; tag.Updating.Value = true; @@ -247,17 +245,10 @@ namespace osu.Game.Screens.Ranking break; } - request.Success += () => - { - tag.Updating.Value = false; - requestInFlight = null; - }; - request.Failure += _ => - { - tag.Updating.Value = false; - requestInFlight = null; - }; - api.Queue(requestInFlight = request); + request.Success += () => tag.Updating.Value = false; + request.Failure += _ => tag.Updating.Value = false; + + api.Queue(request); } private void voteCountChanged(ValueChangedEvent _) From d34a1fb448b7b59185c0b97b46a97a1e2b60e990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 13:44:42 +0200 Subject: [PATCH 139/281] Fix nullability inspection --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 323cdcfc3d..f904306fbc 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Setup public override LocalisableString Title => EditorSetupStrings.MetadataHeader; [Resolved] - private Editor editor { get; set; } + private Editor editor { get; set; } = null!; [BackgroundDependencyLoader] private void load(SetupScreen? setupScreen) From 881785900a7ec3b59d446d9a41785beed80479a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Apr 2025 20:51:22 +0900 Subject: [PATCH 140/281] Don't use bindables when binding isn't happening --- osu.Game/Overlays/MarqueeContainer.cs | 49 +++++++++++++++++++------ osu.Game/Overlays/Music/PlaylistItem.cs | 8 ++-- osu.Game/Overlays/NowPlayingOverlay.cs | 30 ++++++--------- 3 files changed, 53 insertions(+), 34 deletions(-) diff --git a/osu.Game/Overlays/MarqueeContainer.cs b/osu.Game/Overlays/MarqueeContainer.cs index 69ac5f7d06..2fed87c4e0 100644 --- a/osu.Game/Overlays/MarqueeContainer.cs +++ b/osu.Game/Overlays/MarqueeContainer.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; @@ -16,14 +15,35 @@ namespace osu.Game.Overlays /// Whether the marquee should be allowed to scroll the content if it overflows. /// Note that upon changing the value of this, any existing scrolls will be terminated instantly. /// - public Bindable AllowScrolling { get; } = new BindableBool(true); + public bool AllowScrolling + { + get => allowScrolling; + set + { + allowScrolling = value; + ScheduleAfterChildren(updateScrolling); + } + } + + private bool allowScrolling = true; + /// /// The to anchor the content to if it does not overflow. /// public Anchor NonOverflowingContentAnchor { get; init; } = Anchor.TopLeft; - public Bindable> CreateContent = new Bindable>(); + public Func? CreateContent + { + set + { + createContent = value; + if (IsLoaded) + updateContent(); + } + } + + private Func? createContent; private const float initial_move_delay = 1000; private const float pixels_per_second = 50; @@ -57,21 +77,26 @@ namespace osu.Game.Overlays { base.LoadComplete(); - AllowScrolling.BindValueChanged(_ => ScheduleAfterChildren(updateScrolling)); - CreateContent.BindValueChanged(_ => - { - flow.Clear(); - flow.Add(mainContent = CreateContent.Value.Invoke()); - flow.Add(fillerContent = CreateContent.Value.Invoke().With(d => d.Alpha = 0)); - ScheduleAfterChildren(updateScrolling); - }, true); + updateContent(); + } + + private void updateContent() + { + flow.Clear(); + + if (createContent == null) + return; + + flow.Add(mainContent = createContent()); + flow.Add(fillerContent = createContent().With(d => d.Alpha = 0)); + ScheduleAfterChildren(updateScrolling); } private void updateScrolling() { float overflowWidth = mainContent.DrawWidth + padding - DrawWidth; - if (overflowWidth > 0 && AllowScrolling.Value) + if (overflowWidth > 0 && AllowScrolling) { fillerContent.Alpha = 1; flow.Anchor = Anchor.TopLeft; diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 8503a078e1..5b37e36b16 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Music Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, - AllowScrolling = { Value = false } + AllowScrolling = false, }; selectedSet.BindTo(playlistOverlay.SelectedSet); @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Music var title = new RomanisableString(metadata.TitleUnicode, metadata.Title); var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - text.CreateContent.Value = () => + text.CreateContent = () => { var flow = new OsuTextFlowContainer { @@ -113,13 +113,13 @@ namespace osu.Game.Overlays.Music protected override bool OnHover(HoverEvent e) { - text.AllowScrolling.Value = true; + text.AllowScrolling = true; return true; } protected override void OnHoverLost(HoverLostEvent e) { - text.AllowScrolling.Value = false; + text.AllowScrolling = false; base.OnHoverLost(e); } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 24dffdc066..11819cb485 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -113,15 +113,12 @@ namespace osu.Game.Overlays Anchor = Anchor.TopCentre, Position = new Vector2(0, 40), Colour = Color4.White, - CreateContent = + CreateContent = () => new OsuSpriteText { - Value = () => new OsuSpriteText - { - Font = title_font, - Text = @"Nothing to play", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Font = title_font, + Text = @"Nothing to play", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }, NonOverflowingContentAnchor = Anchor.Centre, }, @@ -131,15 +128,12 @@ namespace osu.Game.Overlays Anchor = Anchor.TopCentre, Position = new Vector2(0, 45), Colour = Color4.White, - CreateContent = + CreateContent = () => new OsuSpriteText { - Value = () => new OsuSpriteText - { - Font = artist_font, - Text = @"Nothing to play", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Font = artist_font, + Text = @"Nothing to play", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }, NonOverflowingContentAnchor = Anchor.Centre, }, @@ -338,14 +332,14 @@ namespace osu.Game.Overlays { BeatmapMetadata metadata = beatmap.Metadata; - title.CreateContent.Value = () => new OsuSpriteText + title.CreateContent = () => new OsuSpriteText { Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), Font = title_font, Anchor = Anchor.Centre, Origin = Anchor.Centre, }; - artist.CreateContent.Value = () => new OsuSpriteText + artist.CreateContent = () => new OsuSpriteText { Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), Font = artist_font, From 60e6b56b669eac59eded523bc83b20a7bc02f70b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Apr 2025 20:51:32 +0900 Subject: [PATCH 141/281] Don't delay scroll on hover of playlist items --- osu.Game/Overlays/MarqueeContainer.cs | 7 +++++-- osu.Game/Overlays/Music/PlaylistItem.cs | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MarqueeContainer.cs b/osu.Game/Overlays/MarqueeContainer.cs index 2fed87c4e0..2f3e118b04 100644 --- a/osu.Game/Overlays/MarqueeContainer.cs +++ b/osu.Game/Overlays/MarqueeContainer.cs @@ -27,6 +27,10 @@ namespace osu.Game.Overlays private bool allowScrolling = true; + /// + /// Time in milliseconds before scrolling begins. + /// + public double InitialMoveDelay { get; set; } = 1000; /// /// The to anchor the content to if it does not overflow. @@ -45,7 +49,6 @@ namespace osu.Game.Overlays private Func? createContent; - private const float initial_move_delay = 1000; private const float pixels_per_second = 50; private const float padding = 15; @@ -105,7 +108,7 @@ namespace osu.Game.Overlays float targetX = mainContent.DrawWidth + padding; flow.MoveToX(0) - .Delay(initial_move_delay) + .Delay(InitialMoveDelay) .MoveToX(-targetX, targetX * 1000 / pixels_per_second) .Loop(); } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 5b37e36b16..6217a9bc9e 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -47,6 +47,7 @@ namespace osu.Game.Overlays.Music Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, + InitialMoveDelay = 0, AllowScrolling = false, }; From c42d7aa4fa8e8b0c85fc0129282351dd167c5b35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Apr 2025 20:55:39 +0900 Subject: [PATCH 142/281] Animate scroll back to zero --- osu.Game/Overlays/MarqueeContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MarqueeContainer.cs b/osu.Game/Overlays/MarqueeContainer.cs index 2f3e118b04..1b0b59abe0 100644 --- a/osu.Game/Overlays/MarqueeContainer.cs +++ b/osu.Game/Overlays/MarqueeContainer.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays { fillerContent.Alpha = 0; flow.ClearTransforms(); - flow.X = 0; + flow.MoveToX(0, 300, Easing.OutQuint); flow.Anchor = NonOverflowingContentAnchor; flow.Origin = NonOverflowingContentAnchor; } From 71af50f67541fcac80c4e1e97b8028aad761a3c3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 7 Apr 2025 20:55:50 +0900 Subject: [PATCH 143/281] Validate state with lesser magic --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f1736903df..f1eeae2d61 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -518,9 +518,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - if (beatmapAvailabilityTracker.Availability.Value.State != DownloadState.LocallyAvailable) - return; + // Ensure all the gameplay states are up-to-date, forgoing any misordering/scheduling shenanigans. + updateGameplayState(); + // ... And then check that the set gameplay state is valid. + // When spectating, we'll receive LoadRequested() from the server even though we may not yet have the beatmap. + // In that case, this method will be invoked again after availability changes in onBeatmapAvailabilityChanged(). + if (Beatmap.IsDefault) + { + Logger.Log("Aborting gameplay start - beatmap not downloaded."); + return; + } + + // Start the gameplay session. sampleStart?.Play(); int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); From 23c711d1bc54ac660a94cc0491568f6a969c02d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 14:39:37 +0200 Subject: [PATCH 144/281] Cache aquamarine colour provider at results screen To fix the user tag control. I would have done it locally to the user tag control, but it was pissing me off because I wanted the add button to be aquamarine (as it's closer to the accents the screen is already using), but the popover on open was for whatever reason purple and I just want consistency where possible. --- osu.Game/Screens/Ranking/ResultsScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 6da731588f..8d5e6c05c3 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -86,6 +86,9 @@ namespace osu.Game.Screens.Ranking private Sample? popInSample; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + protected ResultsScreen(ScoreInfo? score) { Score = score; From cded1311c8a70ddf9381c92677d08f352fb5c65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 14:45:38 +0200 Subject: [PATCH 145/281] Allow tag control popover to attach to bottom too Looks bad when trying to tag on a replay otherwise. --- osu.Game/Screens/Ranking/UserTagControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 797d66b5c5..789e2cce9f 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -511,6 +511,7 @@ namespace osu.Game.Screens.Ranking AllowableAnchors = new[] { Anchor.TopCentre, + Anchor.BottomCentre, }; Children = new Drawable[] From 11e48f9f8e805ffbaffeac41fded9cf29f5721d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 14:48:26 +0200 Subject: [PATCH 146/281] Close popovers on hiding the statistics view Now that the tagging popover is back, https://github.com/ppy/osu/issues/32630 actually needs fixing. --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index ad868e58f0..c33514e343 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -304,7 +304,10 @@ namespace osu.Game.Screens.Ranking.Statistics this.FadeOut(250, Easing.OutQuint); if (wasOpened) + { popOutSample?.Play(); + this.HidePopover(); // targeted at the user tag control + } } protected override void Dispose(bool isDisposing) From 9643beafa7cae1ad041844f3fcd6a590a075d3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 14:49:55 +0200 Subject: [PATCH 147/281] Fix crashes in statistics panel test scene --- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index ea80f2c5b2..f92dc0313e 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -26,6 +26,7 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania; @@ -52,6 +53,9 @@ namespace osu.Game.Tests.Visual.Ranking private RulesetStore rulesetStore = null!; private BeatmapManager beatmapManager = null!; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -364,12 +368,16 @@ namespace osu.Game.Tests.Visual.Ranking private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new StatisticsPanel + Child = new PopoverContainer { RelativeSizeAxes = Axes.Both, - State = { Value = Visibility.Visible }, - Score = { Value = score }, - AchievedScore = score, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + AchievedScore = score, + }, }; }); From f968f8ed683466ebada463e2fe473701632b0ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 14:59:51 +0200 Subject: [PATCH 148/281] Fix code quality harder --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index f904306fbc..875b6c7f38 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.Setup private FormTextBox sourceTextBox = null!; private FormTextBox tagsTextBox = null!; - private bool dirty = false; + private bool dirty; public override LocalisableString Title => EditorSetupStrings.MetadataHeader; From 5db4f80e8442d3f02f43cbe5deb6c797700cee8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 7 Apr 2025 15:00:47 +0200 Subject: [PATCH 149/281] Make dependency on editor optional --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 875b6c7f38..735204e2f4 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Setup public override LocalisableString Title => EditorSetupStrings.MetadataHeader; [Resolved] - private Editor editor { get; set; } = null!; + private Editor? editor { get; set; } [BackgroundDependencyLoader] private void load(SetupScreen? setupScreen) @@ -83,7 +83,8 @@ namespace osu.Game.Screens.Edit.Setup }; } - editor.Saved += () => dirty = false; + if (editor != null) + editor.Saved += () => dirty = false; updateReadOnlyState(); } From 7f5295284059f5ff5262198c098fdf032c1e2289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 7 Apr 2025 16:37:25 +0200 Subject: [PATCH 150/281] Add transition when vote-count changes in `UserTagControl` --- osu.Game/Screens/Ranking/UserTagControl.cs | 64 ++++++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index 789e2cce9f..f98ea4a2e2 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -302,7 +302,7 @@ namespace osu.Game.Screens.Ranking protected OsuSpriteText TagCategoryText { get; private set; } = null!; protected OsuSpriteText TagNameText { get; private set; } = null!; - protected OsuSpriteText VoteCountText { get; private set; } = null!; + protected VoteCountText VoteCountText { get; private set; } = null!; private readonly bool showVoteCount; @@ -382,7 +382,8 @@ namespace osu.Game.Screens.Ranking showVoteCount ? new Container { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Children = new Drawable[] @@ -391,9 +392,9 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, }, - VoteCountText = new OsuSpriteText + VoteCountText = new VoteCountText(voteCount) { - Margin = new MarginPadding { Horizontal = 6, Vertical = 3, }, + Margin = new MarginPadding { Horizontal = 6 }, }, } } @@ -418,7 +419,6 @@ namespace osu.Game.Screens.Ranking { voteCount.BindValueChanged(_ => { - VoteCountText.Text = voteCount.Value.ToLocalisableString(); confirmed.Value = voteCount.Value >= 10; }, true); voted.BindValueChanged(v => @@ -731,5 +731,59 @@ namespace osu.Game.Screens.Ranking } } } + + private partial class VoteCountText : CompositeDrawable + { + private OsuSpriteText voteCountText; + + private readonly Bindable voteCount; + + public VoteCountText(Bindable voteCount) + { + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + + this.voteCount = voteCount.GetBoundCopy(); + + AddInternal(voteCountText = createText()); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + voteCount.BindValueChanged(count => + { + var previousText = voteCountText; + + const double transition_duration = 500; + + bool isIncrease = count.NewValue > count.OldValue; + + AddInternal(voteCountText = createText()); + + voteCountText.MoveToY(isIncrease ? 20 : -20) + .MoveToY(0, transition_duration, Easing.OutExpo); + + previousText.BypassAutoSizeAxes = Axes.Both; + previousText.MoveToY(isIncrease ? -20 : 20, transition_duration, Easing.OutExpo).Expire(); + }); + + Scheduler.AddDelayed(() => + { + AutoSizeDuration = 300; + AutoSizeEasing = Easing.OutQuint; + }, 1); + } + + private OsuSpriteText createText() => + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = voteCount.Value.ToLocalisableString(), + }; + } } } From 965f16ef8a4485ec366980a9ef97c0caf70a688a Mon Sep 17 00:00:00 2001 From: sineplusx <112003827+sineplusx@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:14:42 +0200 Subject: [PATCH 151/281] Use actual keybind in multiplayer chat hint --- osu.Game/Localisation/ChatStrings.cs | 4 ++-- .../OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index 6841e7d938..b14cfd6729 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.cs @@ -25,9 +25,9 @@ namespace osu.Game.Localisation public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention"); /// - /// "press enter to chat..." + /// "press {0} to chat..." /// - public static LocalisableString InGameInputPlaceholder => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press enter to chat..."); + public static LocalisableString InGameInputPlaceholder(LocalisableString keyBind) => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press {0} to chat...", keyBind); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index befaf115ae..83c94ab534 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Online.Rooms; @@ -37,14 +38,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer : base(room, leaveChannelOnDispose: false) { RelativeSizeAxes = Axes.X; - Background.Alpha = 0.2f; + } - TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder; + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); TextBox.Focus = () => TextBox.PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder; TextBox.FocusLost = () => { - TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder; + TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); expandedFromTextBoxFocus.Value = false; }; } From 6d833939ae912d1b2409d72b657b53820a6dee97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 13:51:32 +0900 Subject: [PATCH 152/281] Fix incorrect sizing of legacy health display "ki" markers Closes https://github.com/ppy/osu/issues/32724. --- osu.Game/Skinning/LegacyHealthDisplay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 9c06cbbfb5..0d561d6c89 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -234,14 +234,14 @@ namespace osu.Game.Skinning { Bulge(); explode.Blending = isEpic ? BlendingParameters.Additive : BlendingParameters.Inherit; - explode.ScaleTo(1).Then().ScaleTo(isEpic ? 2 : 1.6f, 120); - explode.FadeOutFromOne(120); + explode.ScaleTo(1).Then().ScaleTo(isEpic ? 2 : 1.6f, 120, Easing.Out); + explode.FadeOutFromOne(120, Easing.Out); } public override void Bulge() { base.Bulge(); - Main.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); + Main.ScaleTo(1.2f).Then().ScaleTo(0.8f, 150); } } From 0bb085bce6980fc13cbbbdd31fe11838ab5841c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 14:02:59 +0900 Subject: [PATCH 153/281] Simplify animation application and remove autosizeduration hack --- osu.Game/Screens/Ranking/UserTagControl.cs | 47 ++++++++++------------ 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index f98ea4a2e2..da9dfd66d3 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -734,7 +734,7 @@ namespace osu.Game.Screens.Ranking private partial class VoteCountText : CompositeDrawable { - private OsuSpriteText voteCountText; + private OsuSpriteText? text; private readonly Bindable voteCount; @@ -744,8 +744,6 @@ namespace osu.Game.Screens.Ranking AutoSizeAxes = Axes.X; this.voteCount = voteCount.GetBoundCopy(); - - AddInternal(voteCountText = createText()); } protected override void LoadComplete() @@ -754,36 +752,33 @@ namespace osu.Game.Screens.Ranking voteCount.BindValueChanged(count => { - var previousText = voteCountText; + OsuSpriteText? previousText = text; - const double transition_duration = 500; + AddInternal(text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = voteCount.Value.ToLocalisableString(), + }); - bool isIncrease = count.NewValue > count.OldValue; + if (previousText != null) + { + const double transition_duration = 500; - AddInternal(voteCountText = createText()); + bool isIncrease = count.NewValue > count.OldValue; - voteCountText.MoveToY(isIncrease ? 20 : -20) - .MoveToY(0, transition_duration, Easing.OutExpo); + text.MoveToY(isIncrease ? 20 : -20) + .MoveToY(0, transition_duration, Easing.OutExpo); - previousText.BypassAutoSizeAxes = Axes.Both; - previousText.MoveToY(isIncrease ? -20 : 20, transition_duration, Easing.OutExpo).Expire(); - }); + previousText.BypassAutoSizeAxes = Axes.Both; + previousText.MoveToY(isIncrease ? -20 : 20, transition_duration, Easing.OutExpo).Expire(); - Scheduler.AddDelayed(() => - { - AutoSizeDuration = 300; - AutoSizeEasing = Easing.OutQuint; - }, 1); + AutoSizeDuration = 300; + AutoSizeEasing = Easing.OutQuint; + } + }, true); } - - private OsuSpriteText createText() => - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = voteCount.Value.ToLocalisableString(), - }; } } } From 4b038c37627bcabce7b299f8d60995dbb455be40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 14:12:58 +0900 Subject: [PATCH 154/281] Tidy up flow for retrieving key binding representations --- osu.Game/Configuration/OsuConfigManager.cs | 17 ++++++++--------- osu.Game/OsuGame.cs | 4 ++-- .../Multiplayer/GameplayChatDisplay.cs | 6 ++++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 76d06f3665..d464c97621 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework; using osu.Framework.Bindables; using osu.Framework.Configuration; @@ -35,6 +34,11 @@ namespace osu.Game.Configuration Migrate(); } + /// + /// For a given , return a human-readable string representing the bindings bound to the action. + /// + public LocalisableString LookupKeyBindings(GlobalAction action) => LookupKeyBindingsFunc(action); + protected override void InitialiseDefaults() { // UI/selection defaults @@ -263,10 +267,6 @@ namespace osu.Game.Configuration public override TrackedSettings CreateTrackedSettings() { - // these need to be assigned in normal game startup scenarios. - Debug.Assert(LookupKeyBindings != null); - Debug.Assert(LookupSkinName != null); - return new TrackedSettings { new TrackedSetting(OsuSetting.ShowFpsDisplay, state => new SettingDescription( @@ -308,7 +308,7 @@ namespace osu.Game.Configuration string skinName = string.Empty; if (Guid.TryParse(skin, out var id)) - skinName = LookupSkinName(id); + skinName = LookupSkinNameFunc(id); return new SettingDescription( rawValue: skinName, @@ -329,9 +329,8 @@ namespace osu.Game.Configuration }; } - public Func LookupSkinName { private get; set; } = _ => @"unknown"; - - public Func LookupKeyBindings { get; set; } = _ => @"unknown"; + public Func LookupSkinNameFunc { private get; set; } = _ => @"unknown"; + public Func LookupKeyBindingsFunc { private get; set; } = _ => @"unknown"; IBindable IGameplaySettings.ComboColourNormalisationAmount => GetOriginalBindable(OsuSetting.ComboColourNormalisationAmount); IBindable IGameplaySettings.PositionalHitsoundsLevel => GetOriginalBindable(OsuSetting.PositionalHitsoundsLevel); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3381553970..558242b37b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -979,9 +979,9 @@ namespace osu.Game // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. - LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; + LocalConfig.LookupSkinNameFunc = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; - LocalConfig.LookupKeyBindings = l => + LocalConfig.LookupKeyBindingsFunc = l => { var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 83c94ab534..7b9a4d34ca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -44,13 +44,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); + resetPlaceholderText(); TextBox.Focus = () => TextBox.PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder; TextBox.FocusLost = () => { - TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); + resetPlaceholderText(); expandedFromTextBoxFocus.Value = false; }; + + void resetPlaceholderText() => TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder(config.LookupKeyBindings(GlobalAction.ToggleChatFocus)); } protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor. From 5fc80dbddbe692eff5a61071b50c94205ae8b89c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 14:15:42 +0900 Subject: [PATCH 155/281] Hook up `LocalConfig` functions in `OsuGameBase` to make work in tests --- osu.Game/OsuGame.cs | 14 -------------- osu.Game/OsuGameBase.cs | 13 +++++++++++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 558242b37b..19b80bfba4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -977,20 +977,6 @@ namespace osu.Game MultiplayerClient.PostNotification = n => Notifications.Post(n); MultiplayerClient.PresentMatch = PresentMultiplayerMatch; - // make config aware of how to lookup skins for on-screen display purposes. - // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. - LocalConfig.LookupSkinNameFunc = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; - - LocalConfig.LookupKeyBindingsFunc = l => - { - var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); - - if (combinations.Count == 0) - return ToastStrings.NoKeyBound; - - return string.Join(" / ", combinations); - }; - ScreenFooter.BackReceptor backReceptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4087a8b71e..28a02e0dc2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -421,6 +421,19 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); Beatmap.BindValueChanged(onBeatmapChanged); + + // make config aware of how to lookup skins for on-screen display purposes. + // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. + LocalConfig.LookupSkinNameFunc = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; + LocalConfig.LookupKeyBindingsFunc = l => + { + var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); + + if (combinations.Count == 0) + return ToastStrings.NoKeyBound; + + return string.Join(" / ", combinations); + }; } private void updateLanguage() => CurrentLanguage.Value = LanguageExtensions.GetLanguageFor(frameworkLocale.Value, localisationParameters.Value); From e42301058f92b455dc8a1fc337644089d086fe98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 14:28:48 +0900 Subject: [PATCH 156/281] Fix mouse down actions being leaked through buttons --- osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs | 2 +- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index 48d225de41..ddabd6c9eb 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -122,7 +122,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnMouseDown(MouseDownEvent e) { Content.ScaleTo(ScaleOnMouseDown, 2000, Easing.OutQuint); - return base.OnMouseDown(e); + return true; } protected override void OnMouseUp(MouseUpEvent e) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 22df917992..c09014f2ba 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -470,7 +470,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { marker.Active = true; handleMouseInput(e.ScreenSpaceMousePosition); - return base.OnMouseDown(e); + return true; } protected override void OnMouseUp(MouseUpEvent e) From f2088496c5e092f3e35a0e5f6810a2c1b16c9d1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 14:45:43 +0900 Subject: [PATCH 157/281] Fix editor setup screen sliders not having correct keyboard steps Closes https://github.com/ppy/osu/issues/32691. Fixes some extra cases of failure that aren't pointed out in the issue. --- .../Edit/Setup/CatchDifficultySection.cs | 2 ++ .../Edit/Setup/ManiaDifficultySection.cs | 2 ++ osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs | 3 +++ .../Edit/Setup/TaikoDifficultySection.cs | 2 ++ osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs | 7 +++++++ osu.Game/Screens/Edit/Setup/DifficultySection.cs | 1 + 6 files changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs b/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs index 6ae60c4d24..98e30fa3cc 100644 --- a/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs +++ b/osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs @@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup { Caption = EditorSetupStrings.BaseVelocity, HintText = EditorSetupStrings.BaseVelocityDescription, + KeyboardStep = 0.1f, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { Default = 1.4, @@ -89,6 +90,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup { Caption = EditorSetupStrings.TickRate, HintText = EditorSetupStrings.TickRateDescription, + KeyboardStep = 1, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, diff --git a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs index a5c3c2264c..835a37e064 100644 --- a/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs +++ b/osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs @@ -89,6 +89,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup { Caption = EditorSetupStrings.BaseVelocity, HintText = EditorSetupStrings.BaseVelocityDescription, + KeyboardStep = 0.1f, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { Default = 1.4, @@ -103,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup { Caption = EditorSetupStrings.TickRate, HintText = EditorSetupStrings.TickRateDescription, + KeyboardStep = 1, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, diff --git a/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs b/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs index 45e3f3ac49..3ed1d82883 100644 --- a/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs +++ b/osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs @@ -91,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup { Caption = EditorSetupStrings.BaseVelocity, HintText = EditorSetupStrings.BaseVelocityDescription, + KeyboardStep = 0.1f, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { Default = 1.4, @@ -105,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup { Caption = EditorSetupStrings.TickRate, HintText = EditorSetupStrings.TickRateDescription, + KeyboardStep = 1, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, @@ -119,6 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup { Caption = "Stack Leniency", HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", + KeyboardStep = 0.1f, Current = new BindableFloat(Beatmap.StackLeniency) { Default = 0.7f, diff --git a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs index 52f7176b3f..7f7da92688 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs @@ -60,6 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup { Caption = EditorSetupStrings.BaseVelocity, HintText = EditorSetupStrings.BaseVelocityDescription, + KeyboardStep = 0.1f, Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) { Default = 1.4, @@ -74,6 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup { Caption = EditorSetupStrings.TickRate, HintText = EditorSetupStrings.TickRateDescription, + KeyboardStep = 1, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 4e43b133c7..1304c298fb 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -68,6 +68,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 /// public LocalisableString HintText { get; init; } + /// + /// A custom step value for each key press which actuates a change on this control. + /// + public float KeyboardStep { get; init; } + private Box background = null!; private Box flashLayer = null!; private FormTextBox.InnerTextBox textBox = null!; @@ -140,6 +145,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.X, Width = 0.5f, + KeyboardStep = KeyboardStep, Current = currentNumberInstantaneous, OnCommit = () => current.Value = currentNumberInstantaneous.Value, } @@ -306,6 +312,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Height = 40; RelativeSizeAxes = Axes.X; RangePadding = nub_width / 2; + Children = new Drawable[] { new Container diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 88241451cf..d0fc9cc3e1 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -103,6 +103,7 @@ namespace osu.Game.Screens.Edit.Setup { Caption = EditorSetupStrings.TickRate, HintText = EditorSetupStrings.TickRateDescription, + KeyboardStep = 1, Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) { Default = 1, From 38608360b58ca5ef96daa335c45fcb2ce8bc3a4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 15:12:28 +0900 Subject: [PATCH 158/281] Adjust menu tips and supporter display to not overlap Closes https://github.com/ppy/osu/issues/32684. Also did a very brief pass on timings and animations. --- osu.Game/Screens/Menu/MainMenu.cs | 10 +++++----- .../Screens/Menu/{MenuTip.cs => MenuTipDisplay.cs} | 9 +++++++-- osu.Game/Screens/Menu/SupporterDisplay.cs | 11 ++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) rename osu.Game/Screens/Menu/{MenuTip.cs => MenuTipDisplay.cs} (94%) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 7d792a6bb8..c7d57f2993 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Menu private SongTicker songTicker; private Container logoTarget; private OnlineMenuBanner onlineMenuBanner; - private MenuTip menuTip; + private MenuTipDisplay menuTipDisplay; private FillFlowContainer bottomElementsFlow; private SupporterDisplay supporterDisplay; @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Menu Spacing = new Vector2(5), Children = new Drawable[] { - menuTip = new MenuTip + menuTipDisplay = new MenuTipDisplay { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -206,8 +206,8 @@ namespace osu.Game.Screens.Menu supporterDisplay = new SupporterDisplay { Margin = new MarginPadding(5), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, }, holdToExitGameOverlay?.CreateProxy() ?? Empty() }); @@ -391,7 +391,7 @@ namespace osu.Game.Screens.Menu musicController.EnsurePlayingSomething(); // Cycle tip on resuming - menuTip.ShowNextTip(); + menuTipDisplay.ShowNextTip(); bottomElementsFlow .ScaleTo(1, 1000, Easing.OutQuint) diff --git a/osu.Game/Screens/Menu/MenuTip.cs b/osu.Game/Screens/Menu/MenuTipDisplay.cs similarity index 94% rename from osu.Game/Screens/Menu/MenuTip.cs rename to osu.Game/Screens/Menu/MenuTipDisplay.cs index af7cfde52b..283528d22a 100644 --- a/osu.Game/Screens/Menu/MenuTip.cs +++ b/osu.Game/Screens/Menu/MenuTipDisplay.cs @@ -18,7 +18,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Menu { - public partial class MenuTip : CompositeDrawable + public partial class MenuTipDisplay : CompositeDrawable { [Resolved] private OsuConfigManager config { get; set; } = null!; @@ -86,7 +86,12 @@ namespace osu.Game.Screens.Menu textFlow.AddParagraph(MenuTipStrings.MenuTipTitle, formatSemiBold); textFlow.AddParagraph(tip, formatRegular); - this.FadeInFromZero(200, Easing.OutQuint) + this + .FadeOut() + .ScaleTo(0.9f) + .Delay(600) + .FadeInFromZero(800, Easing.OutQuint) + .ScaleTo(1, 800, Easing.OutElasticHalf) .Delay(1000 + 80 * tip.ToString().Length) .Then() .FadeOutFromOne(2000, Easing.OutQuint); diff --git a/osu.Game/Screens/Menu/SupporterDisplay.cs b/osu.Game/Screens/Menu/SupporterDisplay.cs index be50a54619..9602f4f61d 100644 --- a/osu.Game/Screens/Menu/SupporterDisplay.cs +++ b/osu.Game/Screens/Menu/SupporterDisplay.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -53,7 +54,7 @@ namespace osu.Game.Screens.Menu backgroundBox = new Box { RelativeSizeAxes = Axes.Both, - Alpha = 0.4f, + Alpha = 0.6f, }, supportFlow = new LinkFlowContainer { @@ -111,7 +112,7 @@ namespace osu.Game.Screens.Menu this .FadeOut() - .Delay(1000) + .Delay(RNG.Next(800, 4000)) .FadeInFromZero(800, Easing.OutQuint); scheduleDismissal(); @@ -128,13 +129,13 @@ namespace osu.Game.Screens.Menu protected override bool OnHover(HoverEvent e) { - backgroundBox.FadeTo(0.6f, 500, Easing.OutQuint); + backgroundBox.FadeTo(0.8f, 500, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - backgroundBox.FadeTo(0.4f, 500, Easing.OutQuint); + backgroundBox.FadeTo(0.6f, 500, Easing.OutQuint); base.OnHoverLost(e); } @@ -160,7 +161,7 @@ namespace osu.Game.Screens.Menu this .Delay(200) .FadeOut(750, Easing.Out); - }, 6000); + }, 8000); } } } From e990099cd9f0a770531394a5d550faf02765e89c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 15:26:41 +0900 Subject: [PATCH 159/281] Increase range of vertex selection in polygon generation popover Addresses https://github.com/ppy/osu/discussions/32703. --- osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs index 695ff516b1..046f57c0a5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Edit Current = new BindableNumber(3) { MinValue = 3, - MaxValue = 10, + MaxValue = 32, Precision = 1, }, Instantaneous = true From d31ffd90143e0e71c62d0a2748fd9030f58eb7de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 15:33:43 +0900 Subject: [PATCH 160/281] Include beatmap details in logs when load fails See https://github.com/ppy/osu/discussions/32682#discussioncomment-12760059 for ambiguous logs. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 8df57fd0c8..b0f6082406 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -213,12 +213,12 @@ namespace osu.Game.Beatmaps if (ae.InnerExceptions.FirstOrDefault() is TaskCanceledException) return null; - Logger.Error(ae, "Beatmap failed to load"); + Logger.Error(ae, $"Beatmap failed to load ({BeatmapInfo})"); return null; } catch (Exception e) { - Logger.Error(e, "Beatmap failed to load"); + Logger.Error(e, $"Beatmap failed to load ({BeatmapInfo})"); return null; } } From 5d572e1ab22e8cac1c05060cef2f84a2d3933ead Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Apr 2025 15:47:30 +0900 Subject: [PATCH 161/281] Remove obsoleted overload of `ApplyResult` --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 265d4efac8..ed9ed1db0a 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -707,9 +707,6 @@ namespace osu.Game.Rulesets.Objects.Drawables protected void ApplyResult(HitResult type) => ApplyResult(static (result, state) => result.Type = state, type); - [Obsolete("Use overload with state, preferrably with static delegates to avoid allocation overhead.")] // Can be removed 2024-07-26 - protected void ApplyResult(Action application) => ApplyResult((r, _) => application(r), this); - protected void ApplyResult(Action application) => ApplyResult(application, this); /// From 968fe6e618199cbdc6ded0cd4c9f85f95720a158 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Apr 2025 18:30:35 +0900 Subject: [PATCH 162/281] Add failing countdown start test --- .../TestSceneMultiplayerMatchSubScreen.cs | 34 +++++++++++++++++++ .../Multiplayer/TestMultiplayerClient.cs | 19 +++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 660f84b4d6..2def7aeb1c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -392,6 +393,39 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("flashlight mod panel not activated", () => !this.ChildrenOfType().Single(p => p.Mod is OsuModFlashlight).Active.Value); } + [Test] + public void TestStartCountdown() + { + AddStep("set playlist", () => + { + room.Playlist = + [ + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID + } + ]; + }); + + ClickButtonWhenEnabled(); + + AddUntilStep("wait for room join", () => RoomJoined); + + AddStep("click countdown button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("start a countdown", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single().ChildrenOfType - [Description("Local")] [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))] LocallyModified = -4, + [Description("Unknown")] None = -3, [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))] diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 2a1dd536b8..83b385bb8e 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Drawables { public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip { + /// + /// Whether to show as "unknownn" instead of fading out. + /// + public bool ShowUnknownStatus { get; init; } + public BeatmapOnlineStatus Status { get => status; @@ -93,7 +98,7 @@ namespace osu.Game.Beatmaps.Drawables private void updateState() { - if (Status == BeatmapOnlineStatus.None) + if (Status == BeatmapOnlineStatus.None && !ShowUnknownStatus) { Hide(); return; diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 2c43876fb2..bc3047e624 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -120,6 +120,9 @@ namespace osu.Game.Graphics { switch (status) { + case BeatmapOnlineStatus.None: + return Color4.RosyBrown; + case BeatmapOnlineStatus.LocallyModified: return Color4.OrangeRed; From f9112066d3638477b088e0502665bbb97ede9a3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Apr 2025 17:42:17 +0900 Subject: [PATCH 187/281] Fix carousel handling of bleed areas The idea of specifying "bleed" is to make the carousel aware of its vertical display area. The top bleed is under the filter control; bottom beneath the toolbar. At the end of the day, the point of panel X offset incursion, and the scroll target for current selection, should be at the centre of the screen. The fixes match code which already exists in the previous implementation. Basically, without incorporating `BleedTop` into calculations a second time, the centre position would not match expectations (of being the centre including bleed). --- .../SongSelect/BeatmapCarouselV2TestScene.cs | 2 ++ osu.Game/Screens/SelectV2/Carousel.cs | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 2c902a466f..ad8004304a 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -96,6 +96,8 @@ namespace osu.Game.Tests.Visual.SongSelect { Carousel = new BeatmapCarousel { + BleedTop = 200, + BleedBottom = 200, Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 800, diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 5339b5358b..21310b76a1 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -505,7 +505,7 @@ namespace osu.Game.Screens.SelectV2 private void scrollToSelection() { if (currentKeyboardSelection.CarouselItem != null) - Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight); + Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight + BleedTop); } #endregion @@ -519,17 +519,17 @@ namespace osu.Game.Screens.SelectV2 /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom); + private float visibleBottomBound; /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => (float)(Scroll.Current - BleedTop); + private float visibleUpperBound; /// /// Half the height of the visible content. /// - private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2; + private float visibleHalfHeight; protected override void Update() { @@ -538,6 +538,10 @@ namespace osu.Game.Screens.SelectV2 if (carouselItems == null) return; + visibleBottomBound = (float)(Scroll.Current + DrawHeight + BleedBottom); + visibleUpperBound = (float)(Scroll.Current - BleedTop); + visibleHalfHeight = (DrawHeight + BleedBottom + BleedTop) / 2; + if (!selectionValid.IsValid) { refreshAfterSelection(); @@ -582,7 +586,7 @@ namespace osu.Game.Screens.SelectV2 protected virtual float GetPanelXOffset(Drawable panel) { Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre); - float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight); + float dist = Math.Abs(1f - (posInScroll.Y + BleedTop) / visibleHalfHeight); return offsetX(dist, visibleHalfHeight); } From 430b22b383686441bf6dfae8a3419f620c93c0dd Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Mar 2025 11:41:05 -0400 Subject: [PATCH 188/281] Remove previous beatmap wedge implementation --- .../SongSelectV2/TestSceneBeatmapInfoWedge.cs | 213 ----------- .../TestSceneDifficultyNameContent.cs | 44 --- .../Screens/SelectV2/BeatmapInfoWedgeV2.cs | 330 ------------------ .../SelectV2/Wedge/DifficultyNameContent.cs | 88 ----- .../Wedge/LocalDifficultyNameContent.cs | 34 -- 5 files changed, 709 deletions(-) delete mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs delete mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs delete mode 100644 osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs delete mode 100644 osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs delete mode 100644 osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs deleted file mode 100644 index 5b717887e2..0000000000 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapInfoWedge.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Screens.Select; -using osu.Game.Screens.SelectV2; - -namespace osu.Game.Tests.Visual.SongSelectV2 -{ - public partial class TestSceneBeatmapInfoWedge : SongSelectComponentsTestScene - { - private RulesetStore rulesets = null!; - private TestBeatmapInfoWedgeV2 infoWedge = null!; - private readonly List beatmaps = new List(); - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - this.rulesets = rulesets; - } - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("reset mods", () => SelectedMods.SetDefault()); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AddRange(new Drawable[] - { - // This exists only to make the wedge more visible in the test scene - new Box - { - Y = -20, - Colour = Colour4.Cornsilk.Darken(0.2f), - Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40, - Width = 0.65f, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 20, Left = -10 } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 20 }, - Child = infoWedge = new TestBeatmapInfoWedgeV2 - { - Width = 0.6f, - RelativeSizeAxes = Axes.X, - }, - } - }); - - AddSliderStep("change star difficulty", 0, 11.9, 5.55, v => - { - foreach (var hasCurrentValue in infoWedge.ChildrenOfType>()) - hasCurrentValue.Current.Value = new StarDifficulty(v, 0); - }); - } - - [Test] - public void TestRulesetChange() - { - selectBeatmap(Beatmap.Value.Beatmap); - - AddWaitStep("wait for select", 3); - - foreach (var rulesetInfo in rulesets.AvailableRulesets) - { - var instance = rulesetInfo.CreateInstance(); - var testBeatmap = createTestBeatmap(rulesetInfo); - - beatmaps.Add(testBeatmap); - - setRuleset(rulesetInfo); - - selectBeatmap(testBeatmap); - - testBeatmapLabels(instance); - } - } - - [Test] - public void TestWedgeVisibility() - { - AddStep("hide", () => { infoWedge.Hide(); }); - AddWaitStep("wait for hide", 3); - AddAssert("check visibility", () => infoWedge.Alpha == 0); - AddStep("show", () => { infoWedge.Show(); }); - AddWaitStep("wait for show", 1); - AddAssert("check visibility", () => infoWedge.Alpha > 0); - } - - private void testBeatmapLabels(Ruleset ruleset) - { - AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title"); - AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); - } - - [Test] - public void TestTruncation() - { - selectBeatmap(createLongMetadata()); - } - - [Test] - public void TestNullBeatmapWithBackground() - { - selectBeatmap(null); - AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); - AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); - AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); - } - - private void setRuleset(RulesetInfo rulesetInfo) - { - Container? containerBefore = null; - - AddStep("set ruleset", () => - { - // wedge content is only refreshed if the ruleset changes, so only wait for load in that case. - if (!rulesetInfo.Equals(Ruleset.Value)) - containerBefore = infoWedge.DisplayedContent; - - Ruleset.Value = rulesetInfo; - }); - - AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); - } - - private void selectBeatmap(IBeatmap? b) - { - Container? containerBefore = null; - - AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => - { - containerBefore = infoWedge.DisplayedContent; - infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); - infoWedge.Show(); - }); - - AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); - } - - private IBeatmap createTestBeatmap(RulesetInfo ruleset) - { - List objects = new List(); - for (double i = 0; i < 50000; i += 1000) - objects.Add(new TestHitObject { StartTime = i }); - - return new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Author = { Username = $"{ruleset.ShortName}Author" }, - Artist = $"{ruleset.ShortName}Artist", - Source = $"{ruleset.ShortName}Source", - Title = $"{ruleset.ShortName}Title" - }, - Ruleset = ruleset, - StarRating = 6, - DifficultyName = $"{ruleset.ShortName}Version", - Difficulty = new BeatmapDifficulty() - }, - HitObjects = objects - }; - } - - private IBeatmap createLongMetadata() - { - return new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Author = { Username = "WWWWWWWWWWWWWWW" }, - Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist", - Source = "Verrrrry long Source", - Title = "Verrrrry long Title" - }, - DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version", - Status = BeatmapOnlineStatus.Graveyard, - }, - }; - } - - private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2 - { - public new Container? DisplayedContent => base.DisplayedContent; - public new WedgeInfoText? Info => base.Info; - } - - private class TestHitObject : ConvertHitObject; - } -} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs deleted file mode 100644 index 49e7e2bc1a..0000000000 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyNameContent.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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.Localisation; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Screens.SelectV2.Wedge; - -namespace osu.Game.Tests.Visual.SongSelectV2 -{ - public partial class TestSceneDifficultyNameContent : SongSelectComponentsTestScene - { - private DifficultyNameContent? difficultyNameContent; - - [Test] - public void TestLocalBeatmap() - { - AddStep("set component", () => Child = difficultyNameContent = new LocalDifficultyNameContent()); - - AddAssert("difficulty name is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); - AddAssert("author is not set", () => LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); - - AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - DifficultyName = "really long difficulty name that gets truncated", - Metadata = new BeatmapMetadata - { - Author = { Username = "really long username that is autosized" }, - }, - OnlineID = 1, - } - })); - - AddAssert("difficulty name is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().Text)); - AddAssert("author is set", () => !LocalisableString.IsNullOrEmpty(difficultyNameContent.ChildrenOfType().Single().ChildrenOfType().Single().Text)); - } - } -} diff --git a/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs b/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs deleted file mode 100644 index b294896c77..0000000000 --- a/osu.Game/Screens/SelectV2/BeatmapInfoWedgeV2.cs +++ /dev/null @@ -1,330 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Threading; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; -using osu.Game.Screens.Select; -using osuTK; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class BeatmapInfoWedgeV2 : VisibilityContainer - { - public const float WEDGE_HEIGHT = 120; - private const float shear_width = 21; - private const float transition_duration = 250; - private const float corner_radius = 10; - private const float colour_bar_width = 30; - - /// Todo: move this const out to song select when more new design elements are implemented for the beatmap details area, since it applies to text alignment of various elements - private const float text_margin = 62; - - private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / WEDGE_HEIGHT, 0); - - [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private OsuColour colours { get; set; } = null!; - - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } = null!; - - protected Container? DisplayedContent { get; private set; } - - protected WedgeInfoText? Info { get; private set; } - - private Container difficultyColourBar = null!; - private StarCounter starCounter = null!; - private StarRatingDisplay starRatingDisplay = null!; - private BeatmapSetOnlineStatusPill statusPill = null!; - private Container content = null!; - - private IBindable? starDifficulty; - private CancellationTokenSource? cancellationSource; - - public BeatmapInfoWedgeV2() - { - Height = WEDGE_HEIGHT; - Shear = wedged_container_shear; - Masking = true; - Margin = new MarginPadding { Left = -corner_radius }; - EdgeEffect = new EdgeEffectParameters - { - Colour = Colour4.Black.Opacity(0.2f), - Type = EdgeEffectType.Shadow, - Radius = 3, - }; - CornerRadius = corner_radius; - } - - [BackgroundDependencyLoader] - private void load() - { - Child = content = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // These elements can't be grouped with the rest of the content, due to being present either outside or under the backgrounds area - difficultyColourBar = new Container - { - Colour = Colour4.Transparent, - Depth = float.MaxValue, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - - // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. - Width = colour_bar_width + corner_radius, - Child = new Box { RelativeSizeAxes = Axes.Both } - }, - new Container - { - // Applying the shear to this container and nesting the starCounter inside avoids - // the deformation that occurs if the shear is applied to the starCounter whilst rotated - Shear = -wedged_container_shear, - X = -colour_bar_width / 2, - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = colour_bar_width, - Child = starCounter = new StarCounter - { - Rotation = (float)(Math.Atan(shear_width / WEDGE_HEIGHT) * (180 / Math.PI)), - Colour = Colour4.Transparent, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.35f), - Direction = FillDirection.Vertical - } - }, - new FillFlowContainer - { - Name = "Topright-aligned metadata", - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 3, Right = colour_bar_width + 8 }, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 5), - Depth = float.MinValue, - Children = new Drawable[] - { - starRatingDisplay = new StarRatingDisplay(default, animated: true) - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = -wedged_container_shear, - Alpha = 0, - }, - statusPill = new BeatmapSetOnlineStatusPill - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = -wedged_container_shear, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Alpha = 0, - } - } - }, - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - ruleset.BindValueChanged(_ => updateDisplay()); - - starRatingDisplay.Current.BindValueChanged(s => - { - // use actual stars as star counter has its own animation - starCounter.Current = (float)s.NewValue.Stars; - }, true); - - starRatingDisplay.DisplayedStars.BindValueChanged(s => - { - // sync color with star rating display - starCounter.Colour = s.NewValue >= 6.5 ? colours.Orange1 : Colour4.Black.Opacity(0.75f); - difficultyColourBar.FadeColour(colours.ForStarDifficulty(s.NewValue)); - }, true); - } - - private const double animation_duration = 600; - - protected override void PopIn() - { - this.MoveToX(0, animation_duration, Easing.OutQuint); - this.FadeIn(200, Easing.In); - } - - protected override void PopOut() - { - this.MoveToX(-150, animation_duration, Easing.OutQuint); - this.FadeOut(200, Easing.OutQuint); - } - - private WorkingBeatmap beatmap = null!; - - public WorkingBeatmap Beatmap - { - get => beatmap; - set - { - if (beatmap == value) return; - - beatmap = value; - - updateDisplay(); - } - } - - private Container? loadingInfo; - - private void updateDisplay() - { - statusPill.Status = beatmap.BeatmapInfo.Status; - - starDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, (cancellationSource = new CancellationTokenSource()).Token); - - starDifficulty.BindValueChanged(s => - { - starRatingDisplay.Current.Value = s.NewValue ?? default; - - starRatingDisplay.FadeIn(transition_duration); - }); - - Scheduler.AddOnce(() => - { - LoadComponentAsync(loadingInfo = new Container - { - Padding = new MarginPadding { Right = colour_bar_width }, - RelativeSizeAxes = Axes.Both, - Depth = DisplayedContent?.Depth + 1 ?? 0, - Child = new Container - { - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // TODO: New wedge design uses a coloured horizontal gradient for its background, however this lacks implementation information in the figma draft. - // pending https://www.figma.com/file/DXKwqZhD5yyb1igc3mKo1P?node-id=2980:3361#340801912 being answered. - new BeatmapInfoWedgeBackground(beatmap) { Shear = -Shear }, - Info = new WedgeInfoText(beatmap) { Shear = -Shear } - } - } - }, d => - { - // Ensure we are the most recent loaded wedge. - if (d != loadingInfo) return; - - removeOldInfo(); - content.Add(DisplayedContent = d); - }); - }); - - void removeOldInfo() - { - DisplayedContent?.FadeOut(transition_duration); - DisplayedContent?.Expire(); - DisplayedContent = null; - } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - cancellationSource?.Cancel(); - } - - public partial class WedgeInfoText : Container - { - public OsuSpriteText TitleLabel { get; private set; } = null!; - public OsuSpriteText ArtistLabel { get; private set; } = null!; - - private readonly WorkingBeatmap working; - - public WedgeInfoText(WorkingBeatmap working) - { - this.working = working; - - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(SongSelect? songSelect, LocalisationManager localisation) - { - var metadata = working.Metadata; - - var titleText = new RomanisableString(metadata.TitleUnicode, metadata.Title); - var artistText = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); - - Child = new FillFlowContainer - { - Name = "Top-left aligned metadata", - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Left = text_margin, Top = 12 }, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Children = new Drawable[] - { - new OsuHoverContainer - { - AutoSizeAxes = Axes.Both, - Action = () => songSelect?.Search(titleText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)), - Child = TitleLabel = new TruncatingSpriteText - { - Shadow = true, - Text = titleText, - Font = OsuFont.TorusAlternate.With(size: 40, weight: FontWeight.SemiBold), - }, - }, - new OsuHoverContainer - { - AutoSizeAxes = Axes.Both, - Action = () => songSelect?.Search(artistText.GetPreferred(localisation.CurrentParameters.Value.PreferOriginalScript)), - Child = ArtistLabel = new TruncatingSpriteText - { - // TODO : figma design has a diffused shadow, instead of the solid one present here, not possible currently as far as i'm aware. - Shadow = true, - Text = artistText, - // Not sure if this should be semi bold or medium - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), - }, - }, - } - }; - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - // best effort to confine the auto-sized text to wedge bounds - // the artist label doesn't have an extra text_margin as it doesn't touch the right metadata - TitleLabel.MaxWidth = DrawWidth - text_margin * 2 - shear_width; - ArtistLabel.MaxWidth = DrawWidth - text_margin - shear_width; - } - } - } -} diff --git a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs deleted file mode 100644 index 4a3dc34cf9..0000000000 --- a/osu.Game/Screens/SelectV2/Wedge/DifficultyNameContent.cs +++ /dev/null @@ -1,88 +0,0 @@ -// 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.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Localisation; -using osu.Game.Overlays; - -namespace osu.Game.Screens.SelectV2.Wedge -{ - public abstract partial class DifficultyNameContent : CompositeDrawable - { - protected OsuSpriteText DifficultyName = null!; - private OsuSpriteText mappedByLabel = null!; - protected OsuHoverContainer MapperLink = null!; - protected OsuSpriteText MapperName = null!; - - protected DifficultyNameContent() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - DifficultyName = new TruncatingSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - }, - mappedByLabel = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - // TODO: better null display? beatmap carousel panels also just show this text currently. - Text = " mapped by ", - Font = OsuFont.GetFont(size: 14), - }, - // This is not a `LinkFlowContainer` as there are single-frame layout issues when Update() - // is being used for layout, see https://github.com/ppy/osu-framework/issues/3369. - MapperLink = new MapperLinkContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.Both, - Child = MapperName = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 14), - } - }, - } - }; - } - - protected override void Update() - { - base.Update(); - - // truncate difficulty name when width exceeds bounds, prioritizing mapper name display - DifficultyName.MaxWidth = Math.Max(DrawWidth - mappedByLabel.DrawWidth - - MapperName.DrawWidth, 0); - } - - private partial class MapperLinkContainer : OsuHoverContainer - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours) - { - TooltipText = ContextMenuStrings.ViewProfile; - IdleColour = overlayColourProvider?.Light2 ?? colours.Blue; - } - } - } -} diff --git a/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs b/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs deleted file mode 100644 index 66f8cb02b2..0000000000 --- a/osu.Game/Screens/SelectV2/Wedge/LocalDifficultyNameContent.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.Game.Beatmaps; -using osu.Game.Online; -using osu.Game.Online.Chat; - -namespace osu.Game.Screens.SelectV2.Wedge -{ - public partial class LocalDifficultyNameContent : DifficultyNameContent - { - [Resolved] - private IBindable beatmap { get; set; } = null!; - - [Resolved] - private ILinkHandler? linkHandler { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - beatmap.BindValueChanged(b => - { - DifficultyName.Text = b.NewValue.BeatmapInfo.DifficultyName; - - // TODO: should be the mapper of the guest difficulty, but that isn't stored correctly yet (see https://github.com/ppy/osu/issues/12965) - MapperName.Text = b.NewValue.Metadata.Author.Username; - MapperLink.Action = () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, b.NewValue.Metadata.Author)); - }, true); - } - } -} From ba5932c1dd4eaff2d6e61a9b4dabc3e52dc5e36b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Apr 2025 02:11:45 +0900 Subject: [PATCH 189/281] Revert "Use median for statistic display" This reverts commit fa06643bb6c0aacde659640ae0a65c68ab9b0c61. Revert "Remove mean hit error calculation" This reverts commit b3c578e5455c572e34e2def301ba657182747149. --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 17 +++++++++++++++++ .../Ranking/Statistics/AverageHitError.cs | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 39fc8b357b..01d800a351 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -54,6 +54,23 @@ namespace osu.Game.Rulesets.Scoring return result; } + /// + /// Calculates the average hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. + /// + /// + /// A non-null value if unstable rate could be calculated, + /// and if unstable rate cannot be calculated due to being empty. + /// + public static double? CalculateAverageHitError(this IEnumerable hitEvents) + { + double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + + if (timeOffsets.Length == 0) + return null; + + return timeOffsets.Average(); + } + /// /// Calculates the median hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. /// diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs index 29df085c62..fb7107cc88 100644 --- a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs +++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs @@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Ranking.Statistics { /// - /// Displays the average hit error statistic for a given play. + /// Displays the unstable rate statistic for a given play. /// public partial class AverageHitError : SimpleStatisticItem { /// /// Creates and computes an statistic. /// - /// Sequence of s to calculate the average hit error based on. + /// Sequence of s to calculate the unstable rate based on. public AverageHitError(IEnumerable hitEvents) : base("Average Hit Error") { - Value = hitEvents.CalculateMedianHitError(); + Value = hitEvents.CalculateAverageHitError(); } protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}"; From 6bb84e4364256df75949a56cc4d67023a773f00c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Mar 2025 11:46:42 -0400 Subject: [PATCH 190/281] Update API beatmap model to include user play count --- osu.Game/Online/API/Requests/Responses/APIBeatmap.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 055d2dd8e2..20494a1cbf 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -32,6 +32,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"playcount")] public int PlayCount { get; set; } + [JsonProperty(@"current_user_playcount")] + public int UserPlayCount { get; set; } + [JsonProperty(@"passcount")] public int PassCount { get; set; } From 55dc64e5b6bafcc8b45f4214f432591f5ef888a0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Mar 2025 11:51:23 -0400 Subject: [PATCH 191/281] Add `DarkOrange` colour set --- osu.Game/Graphics/OsuColour.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 2c43876fb2..260ff9f797 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -23,6 +23,7 @@ namespace osu.Game.Graphics /// /// Retrieves the colour for a given point in the star range. /// + // todo: fix stupid array public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[] { (0.1f, Color4Extensions.FromHex("aaaaaa")), @@ -403,6 +404,12 @@ namespace osu.Game.Graphics public readonly Color4 Orange3 = Color4Extensions.FromHex(@"cca633"); public readonly Color4 Orange4 = Color4Extensions.FromHex(@"6b5c2e"); + public readonly Color4 DarkOrange0 = Color4Extensions.FromHex(@"ffbb99"); + public readonly Color4 DarkOrange1 = Color4Extensions.FromHex(@"ff9966"); + public readonly Color4 DarkOrange2 = Color4Extensions.FromHex(@"eb7e47"); + public readonly Color4 DarkOrange3 = Color4Extensions.FromHex(@"cc6633"); + public readonly Color4 DarkOrange4 = Color4Extensions.FromHex(@"6b422e"); + public readonly Color4 Red0 = Color4Extensions.FromHex(@"ff9b9b"); public readonly Color4 Red1 = Color4Extensions.FromHex(@"ff6666"); public readonly Color4 Red2 = Color4Extensions.FromHex(@"eb4747"); From 36a11d4bf7afa3ca238a561aa2e67ed498aed440 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 20 Mar 2025 11:52:13 -0400 Subject: [PATCH 192/281] Add specifications for new song select icons --- osu.Game/Graphics/OsuIcon.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Graphics/OsuIcon.cs b/osu.Game/Graphics/OsuIcon.cs index 9879ef5d14..84ff86a5e5 100644 --- a/osu.Game/Graphics/OsuIcon.cs +++ b/osu.Game/Graphics/OsuIcon.cs @@ -115,6 +115,7 @@ namespace osu.Game.Graphics public static IconUsage ChangelogB => get(OsuIconMapping.ChangelogB); public static IconUsage Chat => get(OsuIconMapping.Chat); public static IconUsage CheckCircle => get(OsuIconMapping.CheckCircle); + public static IconUsage Clock => get(OsuIconMapping.Clock); public static IconUsage CollapseA => get(OsuIconMapping.CollapseA); public static IconUsage Collections => get(OsuIconMapping.Collections); public static IconUsage Cross => get(OsuIconMapping.Cross); @@ -141,6 +142,7 @@ namespace osu.Game.Graphics public static IconUsage Input => get(OsuIconMapping.Input); public static IconUsage Maintenance => get(OsuIconMapping.Maintenance); public static IconUsage Megaphone => get(OsuIconMapping.Megaphone); + public static IconUsage Metronome => get(OsuIconMapping.Metronome); public static IconUsage Music => get(OsuIconMapping.Music); public static IconUsage News => get(OsuIconMapping.News); public static IconUsage Next => get(OsuIconMapping.Next); @@ -204,6 +206,9 @@ namespace osu.Game.Graphics [Description(@"check-circle")] CheckCircle, + [Description(@"clock")] + Clock, + [Description(@"collapse-a")] CollapseA, @@ -282,6 +287,9 @@ namespace osu.Game.Graphics [Description(@"megaphone")] Megaphone, + [Description(@"metronome")] + Metronome, + [Description(@"music")] Music, From 1dbcbbde1538876bbd04e93e66348c1e547b44e4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 23 Mar 2025 23:02:38 -0400 Subject: [PATCH 193/281] Shorten beatmap hit statistics names --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs | 6 +++--- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs | 4 ++-- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 7 +++---- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs | 6 +++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index 1f05d66b86..01cec1d815 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -21,19 +21,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { new BeatmapStatistic { - Name = @"Fruit Count", + Name = @"Fruits", Content = fruits.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), }, new BeatmapStatistic { - Name = @"Juice Stream Count", + Name = @"Juice Streams", Content = juiceStreams.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), }, new BeatmapStatistic { - Name = @"Banana Shower Count", + Name = @"Banana Showers", Content = bananaShowers.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 8222e5477d..8ddcfa128a 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -41,13 +41,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { new BeatmapStatistic { - Name = @"Note Count", + Name = @"Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), }, new BeatmapStatistic { - Name = @"Hold Note Count", + Name = @"Hold Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdNotes.ToString(), }, diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index a5282877ee..730a194751 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Beatmaps @@ -21,19 +20,19 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { new BeatmapStatistic { - Name = BeatmapsetsStrings.ShowStatsCountCircles, + Name = "Circles", Content = circles.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), }, new BeatmapStatistic { - Name = BeatmapsetsStrings.ShowStatsCountSliders, + Name = "Sliders", Content = sliders.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), }, new BeatmapStatistic { - Name = @"Spinner Count", + Name = @"Spinners", Content = spinners.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index 41fe63a553..0781485ab8 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -20,19 +20,19 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { new BeatmapStatistic { - Name = @"Hit Count", + Name = @"Hits", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), }, new BeatmapStatistic { - Name = @"Drumroll Count", + Name = @"Drumrolls", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumRolls.ToString(), }, new BeatmapStatistic { - Name = @"Swell Count", + Name = @"Swells", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), } From 5c54e57d6d8aa803d3794be3581c81c73af77a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 08:01:34 +0200 Subject: [PATCH 194/281] Remove redundant initialisers --- osu.Game/Screens/SelectV2/Carousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Screens/SelectV2/Carousel.cs index 21310b76a1..7b1fd6e999 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Screens/SelectV2/Carousel.cs @@ -38,12 +38,12 @@ namespace osu.Game.Screens.SelectV2 /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. /// - public float BleedTop { get; set; } = 0; + public float BleedTop { get; set; } /// /// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it. /// - public float BleedBottom { get; set; } = 0; + public float BleedBottom { get; set; } /// /// The number of pixels outside the carousel's vertical bounds to manifest drawables. From 9911e0819eabe986eabe310cfac97ff6330e36c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 10:13:49 +0200 Subject: [PATCH 195/281] Reduce bleed in tests to allow them to pass --- .../Visual/SongSelect/BeatmapCarouselV2TestScene.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index ad8004304a..f2faeab1c4 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -96,8 +96,8 @@ namespace osu.Game.Tests.Visual.SongSelect { Carousel = new BeatmapCarousel { - BleedTop = 200, - BleedBottom = 200, + BleedTop = 50, + BleedBottom = 50, Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 800, From c52dc9ffe86086e9728e06dd7a0ab0eec816f32c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Apr 2025 02:33:27 +0900 Subject: [PATCH 196/281] Update difficulty spectrum retrieval function --- osu.Game/Graphics/OsuColour.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 260ff9f797..dec16d65bd 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -20,13 +20,9 @@ namespace osu.Game.Graphics public static Color4 Gray(float amt) => new Color4(amt, amt, amt, 1f); public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255); - /// - /// Retrieves the colour for a given point in the star range. - /// - // todo: fix stupid array - public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[] + public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM = { - (0.1f, Color4Extensions.FromHex("aaaaaa")), + (0.0f, Color4Extensions.FromHex("4290fb")), (0.1f, Color4Extensions.FromHex("4290fb")), (1.25f, Color4Extensions.FromHex("4fc0ff")), (2.0f, Color4Extensions.FromHex("4fffd5")), @@ -38,7 +34,19 @@ namespace osu.Game.Graphics (6.7f, Color4Extensions.FromHex("6563de")), (7.7f, Color4Extensions.FromHex("18158e")), (9.0f, Color4.Black), - }, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); + (10.0f, Color4.Black), + }; + + /// + /// Retrieves the colour for a given point in the star range. + /// + public Color4 ForStarDifficulty(double starDifficulty, bool showGrayOnZero = true) + { + if (showGrayOnZero && starDifficulty < 0.1f) + return Color4Extensions.FromHex("aaaaaa"); + + return ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); + } /// /// Retrieves the colour for a . From ac36e228b822cb5ce5f585b1a64aded5a62ebe6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Apr 2025 14:47:47 +0200 Subject: [PATCH 197/281] Add test exercising osu! replay playback stability after encode --- .../TestSceneReplayStability.cs | 187 ++++++++++++++++++ osu.Game/Screens/Play/GameplayState.cs | 2 + 2 files changed, 189 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs new file mode 100644 index 0000000000..1bd18a59dc --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneReplayStability : RateAdjustedBeatmapTestScene + { + private ReplayPlayer currentPlayer = null!; + private readonly List results = new List(); + + private static readonly object[][] test_cases = new[] + { + // OD = 5 test cases. + // GREAT hit window is [ -50ms, 50ms] + // OK hit window is [-100ms, 100ms] + // MEH hit window is [-150ms, 150ms] + // MISS hit window is [-400ms, 400ms] + new object[] { 5f, 49d, HitResult.Great }, + new object[] { 5f, 49.2d, HitResult.Great }, + new object[] { 5f, 49.7d, HitResult.Great }, + new object[] { 5f, 50d, HitResult.Great }, + new object[] { 5f, 50.4d, HitResult.Ok }, + new object[] { 5f, 50.9d, HitResult.Ok }, + new object[] { 5f, 51d, HitResult.Ok }, + new object[] { 5f, 99d, HitResult.Ok }, + new object[] { 5f, 99.2d, HitResult.Ok }, + new object[] { 5f, 99.7d, HitResult.Ok }, + new object[] { 5f, 100d, HitResult.Ok }, + new object[] { 5f, 100.4d, HitResult.Meh }, + new object[] { 5f, 100.9d, HitResult.Meh }, + new object[] { 5f, 101d, HitResult.Meh }, + new object[] { 5f, 149d, HitResult.Meh }, + new object[] { 5f, 149.2d, HitResult.Meh }, + new object[] { 5f, 149.7d, HitResult.Meh }, + new object[] { 5f, 150d, HitResult.Meh }, + new object[] { 5f, 150.4d, HitResult.Miss }, + new object[] { 5f, 150.9d, HitResult.Miss }, + new object[] { 5f, 151d, HitResult.Miss }, + + // OD = 5.7 test cases. + // GREAT hit window is [ -45.8ms, 45.8ms] + // OK hit window is [ -94.4ms, 94.4ms] + // MEH hit window is [-143.0ms, 143.0ms] + // MISS hit window is [-400.0ms, 400.0ms] + new object[] { 5.7f, 45d, HitResult.Great }, + new object[] { 5.7f, 45.2d, HitResult.Great }, + new object[] { 5.7f, 45.8d, HitResult.Great }, + new object[] { 5.7f, 45.9d, HitResult.Ok }, + new object[] { 5.7f, 46d, HitResult.Ok }, + new object[] { 5.7f, 46.4d, HitResult.Ok }, + new object[] { 5.7f, 94d, HitResult.Ok }, + new object[] { 5.7f, 94.2d, HitResult.Ok }, + new object[] { 5.7f, 94.4d, HitResult.Ok }, + new object[] { 5.7f, 94.48d, HitResult.Ok }, + new object[] { 5.7f, 94.9d, HitResult.Meh }, + new object[] { 5.7f, 95d, HitResult.Meh }, + new object[] { 5.7f, 95.4d, HitResult.Meh }, + new object[] { 5.7f, 142d, HitResult.Meh }, + new object[] { 5.7f, 142.7d, HitResult.Meh }, + new object[] { 5.7f, 143d, HitResult.Meh }, + new object[] { 5.7f, 143.4d, HitResult.Miss }, + new object[] { 5.7f, 143.9d, HitResult.Miss }, + new object[] { 5.7f, 144d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double hit_circle_time = 100; + + Score originalScore = null!; + Score decodedScore = null!; + IBeatmap beatmap = null!; + + AddStep("create beatmap", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap = new OsuBeatmap + { + HitObjects = + { + new HitCircle + { + StartTime = hit_circle_time, + Position = OsuPlayfield.BASE_SIZE / 2 + } + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + }); + }); + AddStep("create replay", () => + { + originalScore = new Score + { + Replay = new Replay + { + Frames = + { + new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), + new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2), + } + } + }; + }); + + AddStep("push player", () => pushNewPlayer(originalScore)); + + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert("Collected one judgement result", () => results, () => Has.Count.EqualTo(1)); + AddAssert("Judgement result is correct", () => results.Single().Type, () => Is.EqualTo(expectedResult)); + + AddStep("exit player", () => currentPlayer.Exit()); + + AddStep("encode and decode score", () => + { + var encoder = new LegacyScoreEncoder(originalScore, beatmap); + + using (var stream = new MemoryStream()) + { + encoder.Encode(stream, leaveOpen: true); + stream.Position = 0; + decodedScore = new TestScoreDecoder(Beatmap.Value).Parse(stream); + } + }); + + AddStep("push player", () => pushNewPlayer(decodedScore)); + + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert("Collected one judgement result", () => results, () => Has.Count.EqualTo(1)); + AddAssert("Judgement result is correct", () => results.Single().Type, () => Is.EqualTo(expectedResult)); + } + + private void pushNewPlayer(Score score) + { + var player = new ReplayPlayer(score); + player.OnLoadComplete += _ => + { + player.GameplayState.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == player) + results.Add(result); + }; + }; + LoadScreen(currentPlayer = player); + results.Clear(); + } + + private class TestScoreDecoder : LegacyScoreDecoder + { + private readonly WorkingBeatmap beatmap; + + public TestScoreDecoder(WorkingBeatmap beatmap) + { + this.beatmap = beatmap; + } + + protected override Ruleset GetRuleset(int rulesetId) => new OsuRuleset(); + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap; + } + } +} diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 851e95495f..80546ef6da 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -62,6 +62,8 @@ namespace osu.Game.Screens.Play /// public bool HasQuit { get; set; } + public bool HasCompleted => HasPassed || HasFailed || HasQuit; + /// /// A bindable tracking the last judgement result applied to any hit object. /// From 30bb8e335c399945db240856f9598ad2a181929c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 10:42:34 +0200 Subject: [PATCH 198/281] Abstractify test scene I think in this case it's genuinely reasonable to use abstracts to reduce boilerplate. --- .../TestSceneReplayStability.cs | 118 +++--------------- .../Tests/Visual/ReplayStabilityTestScene.cs | 106 ++++++++++++++++ 2 files changed, 126 insertions(+), 98 deletions(-) create mode 100644 osu.Game/Tests/Visual/ReplayStabilityTestScene.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs index 1bd18a59dc..8af12fbe2f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -1,33 +1,21 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.IO; -using System.Linq; using NUnit.Framework; -using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Replays; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { - [TestFixture] [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] - public partial class TestSceneReplayStability : RateAdjustedBeatmapTestScene + public partial class TestSceneReplayStability : ReplayStabilityTestScene { - private ReplayPlayer currentPlayer = null!; - private readonly List results = new List(); - private static readonly object[][] test_cases = new[] { // OD = 5 test cases. @@ -88,100 +76,34 @@ namespace osu.Game.Rulesets.Osu.Tests { const double hit_circle_time = 100; - Score originalScore = null!; - Score decodedScore = null!; - IBeatmap beatmap = null!; - - AddStep("create beatmap", () => + var beatmap = new OsuBeatmap { - Beatmap.Value = CreateWorkingBeatmap(beatmap = new OsuBeatmap + HitObjects = { - HitObjects = + new HitCircle { - new HitCircle - { - StartTime = hit_circle_time, - Position = OsuPlayfield.BASE_SIZE / 2 - } - }, - Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, - BeatmapInfo = - { - Ruleset = new OsuRuleset().RulesetInfo, - }, - }); - }); - AddStep("create replay", () => - { - originalScore = new Score - { - Replay = new Replay - { - Frames = - { - new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2), - new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), - new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2), - } + StartTime = hit_circle_time, + Position = OsuPlayfield.BASE_SIZE / 2 } - }; - }); - - AddStep("push player", () => pushNewPlayer(originalScore)); - - AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("Wait for completion", () => currentPlayer.GameplayState.HasCompleted); - AddAssert("Collected one judgement result", () => results, () => Has.Count.EqualTo(1)); - AddAssert("Judgement result is correct", () => results.Single().Type, () => Is.EqualTo(expectedResult)); - - AddStep("exit player", () => currentPlayer.Exit()); - - AddStep("encode and decode score", () => - { - var encoder = new LegacyScoreEncoder(originalScore, beatmap); - - using (var stream = new MemoryStream()) + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = { - encoder.Encode(stream, leaveOpen: true); - stream.Position = 0; - decodedScore = new TestScoreDecoder(Beatmap.Value).Parse(stream); - } - }); - - AddStep("push player", () => pushNewPlayer(decodedScore)); - - AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("Wait for completion", () => currentPlayer.GameplayState.HasCompleted); - AddAssert("Collected one judgement result", () => results, () => Has.Count.EqualTo(1)); - AddAssert("Judgement result is correct", () => results.Single().Type, () => Is.EqualTo(expectedResult)); - } - - private void pushNewPlayer(Score score) - { - var player = new ReplayPlayer(score); - player.OnLoadComplete += _ => - { - player.GameplayState.ScoreProcessor.NewJudgement += result => - { - if (currentPlayer == player) - results.Add(result); - }; + Ruleset = new OsuRuleset().RulesetInfo, + }, }; - LoadScreen(currentPlayer = player); - results.Clear(); - } - private class TestScoreDecoder : LegacyScoreDecoder - { - private readonly WorkingBeatmap beatmap; - - public TestScoreDecoder(WorkingBeatmap beatmap) + var replay = new Replay { - this.beatmap = beatmap; - } + Frames = + { + new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), + new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2), + } + }; - protected override Ruleset GetRuleset(int rulesetId) => new OsuRuleset(); - protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap; + RunTest(beatmap, replay, [expectedResult]); } } } diff --git a/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs b/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs new file mode 100644 index 0000000000..13abedf611 --- /dev/null +++ b/osu.Game/Tests/Visual/ReplayStabilityTestScene.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + /// + /// The goal of this abstract test class is to ensure that the process of exporting of a replay does not affect its playback. + /// Use to exercise that property. + /// + [HeadlessTest] + [TestFixture] + public abstract partial class ReplayStabilityTestScene : RateAdjustedBeatmapTestScene + { + private ReplayPlayer currentPlayer = null!; + private readonly List results = new List(); + + /// + /// Runs against the supplied + /// and checks that the judgement results recorded match . + /// Then, encodes the , decodes the result of encoding, runs the result of decoding against the supplied , + /// and checks that the judgement results recorded still match . + /// + protected void RunTest(IBeatmap beatmap, Replay replay, IEnumerable expectedResults) + { + Score originalScore = null!; + Score decodedScore = null!; + + AddStep(@"create replay", () => originalScore = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo() + }); + + AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(beatmap)); + AddStep(@"set ruleset", () => Ruleset.Value = beatmap.BeatmapInfo.Ruleset); + AddStep(@"push player", () => pushNewPlayer(originalScore)); + + AddUntilStep(@"wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep(@"wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert(@"judgement results before encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults)); + + AddStep(@"exit player", () => currentPlayer.Exit()); + + AddStep(@"encode and decode score", () => + { + var encoder = new LegacyScoreEncoder(originalScore, beatmap); + + using (var stream = new MemoryStream()) + { + encoder.Encode(stream, leaveOpen: true); + stream.Position = 0; + decodedScore = new TestScoreDecoder(Beatmap.Value).Parse(stream); + } + }); + + AddStep(@"push player", () => pushNewPlayer(decodedScore)); + + AddUntilStep(@"Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep(@"Wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert(@"judgement results after encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults)); + } + + private void pushNewPlayer(Score score) + { + var player = new ReplayPlayer(score); + player.OnLoadComplete += _ => + { + player.GameplayState.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == player) + results.Add(result); + }; + }; + LoadScreen(currentPlayer = player); + results.Clear(); + } + + private class TestScoreDecoder : LegacyScoreDecoder + { + private readonly WorkingBeatmap beatmap; + + public TestScoreDecoder(WorkingBeatmap beatmap) + { + this.beatmap = beatmap; + } + + protected override Ruleset GetRuleset(int rulesetId) => beatmap.BeatmapInfo.Ruleset.CreateInstance(); + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap; + } + } +} From f458aad4108dea82a91c72f18cb5ffe79d83cfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 11:24:31 +0200 Subject: [PATCH 199/281] Add test exercising taiko replay playback stability after encode --- .../TestSceneReplayStability.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs new file mode 100644 index 0000000000..d245fbd28f --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneReplayStability : ReplayStabilityTestScene + { + private static readonly object[][] test_cases = new[] + { + // OD = 5 test cases. + // GREAT hit window is [-35ms, 35ms] + // OK hit window is [-80ms, 80ms] + // MISS hit window is [-95ms, 95ms] + new object[] { 5f, -34d, HitResult.Great }, + new object[] { 5f, -34.2d, HitResult.Great }, + new object[] { 5f, -34.7d, HitResult.Great }, + new object[] { 5f, -35d, HitResult.Great }, + new object[] { 5f, -35.2d, HitResult.Ok }, + new object[] { 5f, -35.8d, HitResult.Ok }, + new object[] { 5f, -36d, HitResult.Ok }, + new object[] { 5f, -79d, HitResult.Ok }, + new object[] { 5f, -79.3d, HitResult.Ok }, + new object[] { 5f, -79.7d, HitResult.Ok }, + new object[] { 5f, -80d, HitResult.Ok }, + new object[] { 5f, -80.2d, HitResult.Miss }, + new object[] { 5f, -80.8d, HitResult.Miss }, + new object[] { 5f, -81d, HitResult.Miss }, + + // OD = 7.8 test cases. + // GREAT hit window is [-26.6ms, 26.6ms] + // OK hit window is [-63.2ms, 63.2ms] + // MISS hit window is [-81.0ms, 81.0ms] + new object[] { 7.8f, -26d, HitResult.Great }, + new object[] { 7.8f, -26.4d, HitResult.Great }, + new object[] { 7.8f, -26.59d, HitResult.Great }, + new object[] { 7.8f, -26.8d, HitResult.Ok }, + new object[] { 7.8f, -27d, HitResult.Ok }, + new object[] { 7.8f, -27.1d, HitResult.Ok }, + new object[] { 7.8f, -63d, HitResult.Ok }, + new object[] { 7.8f, -63.18d, HitResult.Ok }, + new object[] { 7.8f, -63.4d, HitResult.Ok }, + new object[] { 7.8f, -63.7d, HitResult.Miss }, + new object[] { 7.8f, -64d, HitResult.Miss }, + new object[] { 7.8f, -64.2d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double hit_time = 100; + + var beatmap = new TaikoBeatmap() + { + HitObjects = + { + new Hit + { + StartTime = hit_time, + Type = HitType.Centre, + } + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = + { + Ruleset = new TaikoRuleset().RulesetInfo, + }, + }; + + var replay = new Replay + { + Frames = + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre), + new TaikoReplayFrame(hit_time + hitOffset + 20), + } + }; + + RunTest(beatmap, replay, [expectedResult]); + } + } +} From 674af698b613a0af53632de91d7e8004f0ff4a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 12:04:31 +0200 Subject: [PATCH 200/281] Add test exercising mania replay playback stability after encode --- .../TestSceneReplayStability.cs | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs new file mode 100644 index 0000000000..1f51a1494d --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -0,0 +1,143 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneReplayStability : ReplayStabilityTestScene + { + private static readonly object[][] test_cases = new[] + { + // OD = 5 test cases. + // PERFECT hit window is [ -19.4ms, 19.4ms] + // GREAT hit window is [ -49.0ms, 49.0ms] + // GOOD hit window is [ -82.0ms, 82.0ms] + // OK hit window is [-112.0ms, 112.0ms] + // MEH hit window is [-136.0ms, 136.0ms] + // MISS hit window is [-173.0ms, 173.0ms] + new object[] { 5f, -19d, HitResult.Perfect }, + new object[] { 5f, -19.2d, HitResult.Perfect }, + new object[] { 5f, -19.38d, HitResult.Perfect }, + new object[] { 5f, -19.44d, HitResult.Great }, + new object[] { 5f, -19.7d, HitResult.Great }, + new object[] { 5f, -20d, HitResult.Great }, + new object[] { 5f, -48d, HitResult.Great }, + new object[] { 5f, -48.4d, HitResult.Great }, + new object[] { 5f, -48.7d, HitResult.Great }, + new object[] { 5f, -49d, HitResult.Great }, + new object[] { 5f, -49.2d, HitResult.Good }, + new object[] { 5f, -49.7d, HitResult.Good }, + new object[] { 5f, -50d, HitResult.Good }, + new object[] { 5f, -81d, HitResult.Good }, + new object[] { 5f, -81.2d, HitResult.Good }, + new object[] { 5f, -81.7d, HitResult.Good }, + new object[] { 5f, -82d, HitResult.Good }, + new object[] { 5f, -82.2d, HitResult.Ok }, + new object[] { 5f, -82.7d, HitResult.Ok }, + new object[] { 5f, -83d, HitResult.Ok }, + new object[] { 5f, -111d, HitResult.Ok }, + new object[] { 5f, -111.2d, HitResult.Ok }, + new object[] { 5f, -111.7d, HitResult.Ok }, + new object[] { 5f, -112d, HitResult.Ok }, + new object[] { 5f, -112.2d, HitResult.Meh }, + new object[] { 5f, -112.7d, HitResult.Meh }, + new object[] { 5f, -113d, HitResult.Meh }, + new object[] { 5f, -135d, HitResult.Meh }, + new object[] { 5f, -135.2d, HitResult.Meh }, + new object[] { 5f, -135.8d, HitResult.Meh }, + new object[] { 5f, -136d, HitResult.Meh }, + new object[] { 5f, -136.2d, HitResult.Miss }, + new object[] { 5f, -136.7d, HitResult.Miss }, + new object[] { 5f, -137d, HitResult.Miss }, + + // OD = 9.3 test cases. + // PERFECT hit window is [ -14.67ms, 14.67ms] + // GREAT hit window is [ -36.10ms, 36.10ms] + // GOOD hit window is [ -69.10ms, 69.10ms] + // OK hit window is [ -99.10ms, 99.10ms] + // MEH hit window is [-123.10ms, 123.10ms] + // MISS hit window is [-160.10ms, 160.10ms] + new object[] { 9.3f, 14d, HitResult.Perfect }, + new object[] { 9.3f, 14.2d, HitResult.Perfect }, + new object[] { 9.3f, 14.6d, HitResult.Perfect }, + new object[] { 9.3f, 14.7d, HitResult.Great }, + new object[] { 9.3f, 15d, HitResult.Great }, + new object[] { 9.3f, 35d, HitResult.Great }, + new object[] { 9.3f, 35.3d, HitResult.Great }, + new object[] { 9.3f, 35.8d, HitResult.Great }, + new object[] { 9.3f, 36.05d, HitResult.Great }, + new object[] { 9.3f, 36.3d, HitResult.Good }, + new object[] { 9.3f, 36.7d, HitResult.Good }, + new object[] { 9.3f, 37d, HitResult.Good }, + new object[] { 9.3f, 68d, HitResult.Good }, + new object[] { 9.3f, 68.4d, HitResult.Good }, + new object[] { 9.3f, 68.9d, HitResult.Good }, + new object[] { 9.3f, 69.07d, HitResult.Good }, + new object[] { 9.3f, 69.25d, HitResult.Ok }, + new object[] { 9.3f, 69.85d, HitResult.Ok }, + new object[] { 9.3f, 70d, HitResult.Ok }, + new object[] { 9.3f, 98d, HitResult.Ok }, + new object[] { 9.3f, 98.3d, HitResult.Ok }, + new object[] { 9.3f, 98.6d, HitResult.Ok }, + new object[] { 9.3f, 99d, HitResult.Ok }, + new object[] { 9.3f, 99.3d, HitResult.Meh }, + new object[] { 9.3f, 99.7d, HitResult.Meh }, + new object[] { 9.3f, 100d, HitResult.Meh }, + new object[] { 9.3f, 122d, HitResult.Meh }, + new object[] { 9.3f, 122.34d, HitResult.Meh }, + new object[] { 9.3f, 122.57d, HitResult.Meh }, + new object[] { 9.3f, 123.04d, HitResult.Meh }, + new object[] { 9.3f, 123.45d, HitResult.Miss }, + new object[] { 9.3f, 123.95d, HitResult.Miss }, + new object[] { 9.3f, 124d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double note_time = 100; + + var beatmap = new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = + { + new Note() + { + StartTime = note_time, + Column = 0, + } + }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = overallDifficulty, + CircleSize = 1, + }, + BeatmapInfo = + { + Ruleset = new ManiaRuleset().RulesetInfo, + }, + }; + + var replay = new Replay + { + Frames = + { + new ManiaReplayFrame(0), + new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1), + new ManiaReplayFrame(note_time + hitOffset + 20), + } + }; + + RunTest(beatmap, replay, [expectedResult]); + } + } +} From 74227e7b79afb165a49963f187b603863587fe6b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 3 Apr 2025 04:55:34 -0400 Subject: [PATCH 201/281] Define standard font sizes --- osu.Game/Graphics/OsuFont.cs | 52 +++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 7aa98ece95..b314c602f5 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -15,15 +15,65 @@ namespace osu.Game.Graphics /// public const float DEFAULT_FONT_SIZE = 16; + /// + /// Template font styles which should be preferred whenever possible for UI elements. + /// + public static class Style + { + /// + /// Equivalent to Torus with 32px size and semi-bold weight. + /// + public static FontUsage Title => GetFont(Typeface.TorusAlternate, size: 32, weight: FontWeight.Regular); + + /// + /// Torus with 28px size and semi-bold weight. + /// + public static FontUsage Subtitle => GetFont(size: 28, weight: FontWeight.Regular); + + /// + /// Torus with 22px size and bold weight. + /// + public static FontUsage Heading1 => GetFont(size: 22, weight: FontWeight.Bold); + + /// + /// Torus with 18px size and semi-bold weight. + /// + public static FontUsage Heading2 => GetFont(size: 18, weight: FontWeight.SemiBold); + + /// + /// Torus with 16px size and regular weight. + /// + public static FontUsage Body => GetFont(size: DEFAULT_FONT_SIZE, weight: FontWeight.Regular); + + /// + /// Torus with 14px size and regular weight. + /// + public static FontUsage Caption1 => GetFont(size: 14, weight: FontWeight.Regular); + + /// + /// Torus with 12px size and regular weight. + /// + public static FontUsage Caption2 => GetFont(size: 12, weight: FontWeight.Regular); + } + /// /// The default font. /// - public static FontUsage Default => GetFont(); + public static FontUsage Default => GetFont(weight: FontWeight.Medium); + /// + /// Font face for numeric display. + /// public static FontUsage Numeric => GetFont(Typeface.Venera, weight: FontWeight.Bold); + /// + /// Default font face for UI and game elements. + /// public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); + /// + /// Default font face with alternate character set for headings and flair text. + /// public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular); public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular); From 08c17bdf9ec27f9ee8ab35f9c9c2a3ae93b517f3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 07:45:10 -0400 Subject: [PATCH 202/281] Remove conditional in difficulty spectrum retrieval function Wiil be handled locally instead using the diff in https://github.com/ppy/osu/pull/32764#discussion_r2039384833 --- osu.Game/Graphics/OsuColour.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index dec16d65bd..5adecc7182 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -22,7 +22,7 @@ namespace osu.Game.Graphics public static readonly (float, Color4)[] STAR_DIFFICULTY_SPECTRUM = { - (0.0f, Color4Extensions.FromHex("4290fb")), + (0.1f, Color4Extensions.FromHex("aaaaaa")), (0.1f, Color4Extensions.FromHex("4290fb")), (1.25f, Color4Extensions.FromHex("4fc0ff")), (2.0f, Color4Extensions.FromHex("4fffd5")), @@ -40,13 +40,7 @@ namespace osu.Game.Graphics /// /// Retrieves the colour for a given point in the star range. /// - public Color4 ForStarDifficulty(double starDifficulty, bool showGrayOnZero = true) - { - if (showGrayOnZero && starDifficulty < 0.1f) - return Color4Extensions.FromHex("aaaaaa"); - - return ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); - } + public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(STAR_DIFFICULTY_SPECTRUM, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); /// /// Retrieves the colour for a . From c4cfd3a148344665654ca651738d0f96a716b2af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Apr 2025 21:14:56 +0900 Subject: [PATCH 203/281] Fix some incorrect/lacking comments --- .../Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 83b385bb8e..3ee0be61b1 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; +using osuTK; using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables @@ -20,7 +21,7 @@ namespace osu.Game.Beatmaps.Drawables public partial class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip { /// - /// Whether to show as "unknownn" instead of fading out. + /// Whether to show as "unknown" instead of fading out. /// public bool ShowUnknownStatus { get; init; } @@ -104,9 +105,10 @@ namespace osu.Game.Beatmaps.Drawables return; } - // Only animate resizing if we already have a size. - // This avoids animating height from zero. - if (Width > 0) + // The autosize animation on this component is intended to animate horizontal sizing only. + // To avoid vertical autosize animating from zero to non-zero, only apply the duration + // after we have a valid size. + if (Height > 0) { AutoSizeDuration = (float)animation_duration; AutoSizeEasing = Easing.OutQuint; From d4cae3232bde877a5586fc50d70da95aa8cc5a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Apr 2025 14:16:24 +0200 Subject: [PATCH 204/281] Fix code quality --- osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs index 1f51a1494d..497d8a18b8 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { - private static readonly object[][] test_cases = new[] + private static readonly object[][] test_cases = { // OD = 5 test cases. // PERFECT hit window is [ -19.4ms, 19.4ms] @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Mania.Tests { HitObjects = { - new Note() + new Note { StartTime = note_time, Column = 0, diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs index 8af12fbe2f..aca8f757f2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { - private static readonly object[][] test_cases = new[] + private static readonly object[][] test_cases = { // OD = 5 test cases. // GREAT hit window is [ -50ms, 50ms] diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs index d245fbd28f..4a2cd024b0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] public partial class TestSceneReplayStability : ReplayStabilityTestScene { - private static readonly object[][] test_cases = new[] + private static readonly object[][] test_cases = { // OD = 5 test cases. // GREAT hit window is [-35ms, 35ms] @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Tests { const double hit_time = 100; - var beatmap = new TaikoBeatmap() + var beatmap = new TaikoBeatmap { HitObjects = { From 4b9873f03e656e03ca539d5850ca2e6c97fd80ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Apr 2025 21:19:01 +0900 Subject: [PATCH 205/281] Avoid performing colour fades when pill is not visible in the first place --- .../Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 3ee0be61b1..7b3067e8d6 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -13,7 +13,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Overlays; -using osuTK; using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables @@ -116,6 +115,10 @@ namespace osu.Game.Beatmaps.Drawables this.FadeIn(animation_duration, Easing.OutQuint); + // Handle the case where transition from hidden to non-hidden may cause + // a fade from a colour that doesn't make sense (due to not being able to see the previous colour). + double duration = Alpha > 0 ? animation_duration : 0; + Color4 statusTextColour; if (colourProvider != null) @@ -123,8 +126,8 @@ namespace osu.Game.Beatmaps.Drawables else statusTextColour = status == BeatmapOnlineStatus.Graveyard ? colours.GreySeaFoamLight : Color4.Black; - statusText.FadeColour(statusTextColour, animation_duration, Easing.OutQuint); - background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, animation_duration, Easing.OutQuint); + statusText.FadeColour(statusTextColour, duration, Easing.OutQuint); + background.FadeColour(OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter, duration, Easing.OutQuint); statusText.Text = Status.GetLocalisableDescription().ToUpper(); } From 85556e0c3e29e3ccb12f7324db82d18ff949a7fa Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 08:31:03 -0400 Subject: [PATCH 206/281] Introduce numeric value in beatmap hit count statistics --- .../Beatmaps/CatchBeatmap.cs | 5 +++++ .../Beatmaps/ManiaBeatmap.cs | 3 +++ osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 5 +++++ .../Beatmaps/TaikoBeatmap.cs | 5 +++++ osu.Game/Beatmaps/BeatmapStatistic.cs | 17 ++++++++++++++++- 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index 01cec1d815..e9d087929f 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int fruits = HitObjects.Count(s => s is Fruit); int juiceStreams = HitObjects.Count(s => s is JuiceStream); int bananaShowers = HitObjects.Count(s => s is BananaShower); + int sum = Math.Max(1, fruits + juiceStreams + bananaShowers); return new[] { @@ -24,18 +26,21 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Name = @"Fruits", Content = fruits.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), + Ratio = fruits / (float)sum, }, new BeatmapStatistic { Name = @"Juice Streams", Content = juiceStreams.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), + Ratio = juiceStreams / (float)sum, }, new BeatmapStatistic { Name = @"Banana Showers", Content = bananaShowers.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), + Ratio = Math.Min(bananaShowers / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 8ddcfa128a..16e1751e95 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { int notes = HitObjects.Count(s => s is Note); int holdNotes = HitObjects.Count(s => s is HoldNote); + int sum = Math.Max(1, notes + holdNotes); return new[] { @@ -44,12 +45,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Name = @"Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), + Ratio = notes / (float)sum, }, new BeatmapStatistic { Name = @"Hold Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdNotes.ToString(), + Ratio = holdNotes / (float)sum, }, }; } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index 730a194751..cc73f2860a 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps int circles = HitObjects.Count(c => c is HitCircle); int sliders = HitObjects.Count(s => s is Slider); int spinners = HitObjects.Count(s => s is Spinner); + int sum = Math.Max(1, circles + sliders + spinners); return new[] { @@ -23,18 +25,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps Name = "Circles", Content = circles.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), + Ratio = circles / (float)sum, }, new BeatmapStatistic { Name = "Sliders", Content = sliders.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), + Ratio = sliders / (float)sum, }, new BeatmapStatistic { Name = @"Spinners", Content = spinners.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), + Ratio = Math.Min(spinners / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index 0781485ab8..ad4413d84a 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps int hits = HitObjects.Count(s => s is Hit); int drumRolls = HitObjects.Count(s => s is DrumRoll); int swells = HitObjects.Count(s => s is Swell); + int sum = Math.Max(1, hits + drumRolls + swells); return new[] { @@ -23,18 +25,21 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Name = @"Hits", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), + Ratio = hits / (float)sum, }, new BeatmapStatistic { Name = @"Drumrolls", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumRolls.ToString(), + Ratio = drumRolls / (float)sum, }, new BeatmapStatistic { Name = @"Swells", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), + Ratio = Math.Min(swells / 10f, 1), } }; } diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 13e0e4ad5e..6faf74d9c6 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -16,7 +16,22 @@ namespace osu.Game.Beatmaps /// public Func CreateIcon; - public string Content; + /// + /// The name of this statistic. + /// public LocalisableString Name; + + /// + /// The text representing the value of this statistic. + /// + public string Content; + + /// + /// The ratio of this statistic compared to other relevant statistics, or null if not applicable. + /// + /// + /// This is used to display a bar on top of the statistic with the given ratio. + /// + public float? Ratio; } } From a2a1ddaa79fe63bd0b97757b59009b6873182107 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:55:39 -0400 Subject: [PATCH 207/281] Increase group panel height Matches design and also because of the next commit which increases group label size to coexist visually with other panel types. --- osu.Game/Screens/SelectV2/PanelGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index ecb64f4797..800d7a2d07 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class PanelGroup : PanelBase { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.2f; private Drawable chevronIcon = null!; private OsuSpriteText titleText = null!; From d9d3c93a9696e17d006684659ebaa39bd1e929bc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:55:26 -0400 Subject: [PATCH 208/281] Use font specification in group panels --- osu.Game/Screens/SelectV2/PanelGroup.cs | 4 +++- osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index 800d7a2d07..410b6c9e86 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -49,6 +49,8 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Font = OsuFont.Style.Heading2, + UseFullGlyphHeight = false, X = 10f, }, new CircularContainer @@ -69,7 +71,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), // TODO: requires Carousel/CarouselItem-side implementation Text = "43", UseFullGlyphHeight = false, diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 0dc5a2f365..a238539102 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), // TODO: requires Carousel/CarouselItem-side implementation Text = "43", UseFullGlyphHeight = false, From 12e35557a545d8a50fce35268fafed901cb918b0 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:57:20 -0400 Subject: [PATCH 209/281] Update group panel design to match latest iteration --- osu.Game/Screens/SelectV2/PanelGroup.cs | 64 ++++++++++++++++++++----- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index 410b6c9e86..a5786b53c9 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -5,10 +5,12 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; @@ -20,27 +22,56 @@ namespace osu.Game.Screens.SelectV2 { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.2f; - private Drawable chevronIcon = null!; + private Drawable iconContainer = null!; private OsuSpriteText titleText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load() { Height = HEIGHT; - Icon = chevronIcon = new SpriteIcon + Icon = iconContainer = new Container { AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, - Colour = colourProvider.Background3, + RelativeSizeAxes = Axes.Y, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Colour = colourProvider.Background3, + }, }; - Background = new Box + Background = new Container { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5) + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Highlight1, colourProvider.Highlight1.Opacity(0f)), + }, + }, }; AccentColour = colourProvider.Highlight1; Content.Children = new Drawable[] @@ -77,7 +108,7 @@ namespace osu.Game.Screens.SelectV2 UseFullGlyphHeight = false, } }, - } + }, }; } @@ -92,8 +123,15 @@ namespace osu.Game.Screens.SelectV2 { const float duration = 500; - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + ColourInfo colour = Expanded.Value + ? ColourInfo.GradientHorizontal(colourProvider.Highlight1.Opacity(0.25f), colourProvider.Highlight1.Opacity(0f)) + : ColourInfo.GradientHorizontal(colourProvider.Background6, colourProvider.Background5); + + triangles.FadeColour(colour, duration, Easing.OutQuint); + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); } protected override void PrepareForUse() From d3f3c4f6d08303a30ffcefc5c9e182945b4f6fff Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:58:04 -0400 Subject: [PATCH 210/281] Update star rating group panel to look better --- osu.Game/Localisation/SongSelectStrings.cs | 15 +++ .../SelectV2/PanelGroupStarDifficulty.cs | 109 +++++++++++++----- 2 files changed, 93 insertions(+), 31 deletions(-) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index e715ba8880..ecf68e33a8 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -54,6 +54,21 @@ namespace osu.Game.Localisation /// public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap"); + /// + /// "Below 1 Star" + /// + public static LocalisableString BelowStar => new TranslatableString(getKey(@"below_star"), @"Below 1 Star"); + + /// + /// "1 Star" + /// + public static LocalisableString Star => new TranslatableString(getKey(@"star"), @"1 Star"); + + /// + /// "{0} Stars" + /// + public static LocalisableString Stars(int starNumber) => new TranslatableString(getKey(@"stars"), @"{0} Stars", starNumber); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index a238539102..2fba25b3f0 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -5,17 +5,17 @@ using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; +using osu.Game.Localisation; namespace osu.Game.Screens.SelectV2 { @@ -27,28 +27,50 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - private Drawable chevronIcon = null!; + private Drawable iconContainer = null!; private Box contentBackground = null!; - private StarRatingDisplay starRatingDisplay = null!; - private StarCounter starCounter = null!; + private OsuSpriteText starRatingText = null!; + private TrianglesV2 triangles = null!; + private Box glow = null!; [BackgroundDependencyLoader] private void load() { Height = PanelGroup.HEIGHT; - Icon = chevronIcon = new SpriteIcon + Icon = iconContainer = new Container { AlwaysPresent = true, - Icon = FontAwesome.Solid.ChevronDown, - Size = new Vector2(12), - Margin = new MarginPadding { Horizontal = 5f }, - X = 2f, + RelativeSizeAxes = Axes.Y, + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + }, }; - Background = contentBackground = new Box + Background = new Container { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark1, + Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Thickness = 0.02f, + SpawnRatio = 0.6f, + }, + glow = new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + }, + }, }; AccentColour = colourProvider.Highlight1; Content.Children = new Drawable[] @@ -62,17 +84,13 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Left = 10f }, Children = new Drawable[] { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + starRatingText = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - }, - starCounter = new StarCounter - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Scale = new Vector2(8f / 20f), - }, + UseFullGlyphHeight = false, + Font = OsuFont.Style.Heading2, + } } }, new CircularContainer @@ -110,6 +128,8 @@ namespace osu.Game.Screens.SelectV2 Expanded.BindValueChanged(_ => onExpanded(), true); } + private Color4 ratingColour; + protected override void PrepareForUse() { base.PrepareForUse(); @@ -118,25 +138,52 @@ namespace osu.Game.Screens.SelectV2 int starNumber = (int)((GroupDefinition)Item.Model).Data; - Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); - Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + ratingColour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); - AccentColour = colour; - contentBackground.Colour = colour.Darken(0.3f); + AccentColour = ratingColour; + contentBackground.Colour = ratingColour.Darken(1f); + glow.Colour = ColourInfo.GradientHorizontal(ratingColour, ratingColour.Opacity(0f)); - starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0); - starCounter.Current = starNumber; + switch (starNumber) + { + case 0: + starRatingText.Text = SongSelectStrings.BelowStar; + break; - chevronIcon.Colour = contentColour; - starCounter.Colour = contentColour; + case 1: + starRatingText.Text = SongSelectStrings.Star; + break; + + default: + starRatingText.Text = SongSelectStrings.Stars(starNumber); + break; + } + + iconContainer.Colour = starNumber >= 7 ? colourProvider.Content1 : colourProvider.Background5; + starRatingText.Colour = colourProvider.Content1; + + ColourInfo colour; + + if (starNumber >= 8) + colour = ColourInfo.GradientHorizontal(ratingColour, ratingColour.Darken(0.2f)); + else + colour = ColourInfo.GradientHorizontal(ratingColour.Darken(0.6f), ratingColour.Darken(0.8f)); + + triangles.Colour = colour; + + onExpanded(); } private void onExpanded() { const float duration = 500; - chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); - chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + Debug.Assert(Item != null); + + iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); + iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + + glow.FadeTo(Expanded.Value ? 0.4f : 0, duration, Easing.OutQuint); } } } From 6db73b8a13146d05d57254d3a9029eb6a1cd8806 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 07:10:54 -0400 Subject: [PATCH 211/281] Change keyboard selection highlight colour to give betetr visuals --- osu.Game/Screens/SelectV2/PanelBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs index 05a1a55c03..32da02a189 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -159,7 +159,7 @@ namespace osu.Game.Screens.SelectV2 keyboardSelectionLayer = new Box { Alpha = 0, - Colour = colours.Yellow.Opacity(0.1f), + Colour = colourProvider.Highlight1.Opacity(0.1f), Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, }, From b95b9b36430fd5213bdf717c68308c45ac5f079a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 11 Apr 2025 06:58:10 -0400 Subject: [PATCH 212/281] Improve group panel test scene --- .../TestSceneBeatmapCarouselV2GroupPanel.cs | 90 +++++++++++++------ 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs index 9b07f01e52..d62aee77f3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Visual.UserInterface; using osuTK; @@ -16,6 +19,66 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { } + [Test] + public void TestGeneral() + { + AddStep("general", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); + } + + [Test] + public void TestStars() + { + for (int i = 0; i <= 10; i++) + { + int star = i; + + AddStep($"display {i} star(s)", () => + { + ContentContainer.Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine)) + }, + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new[] + { + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())) + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + KeyboardSelected = { Value = true }, + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + Expanded = { Value = true }, + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(star, star.ToString())), + Expanded = { Value = true }, + KeyboardSelected = { Value = true }, + }, + }, + } + }; + }); + } + } + protected override Drawable CreateContent() { return new FillFlowContainer @@ -49,33 +112,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 KeyboardSelected = { Value = true }, Expanded = { Value = true } }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(1, "1")) - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(3, "3")), - Expanded = { Value = true } - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(5, "5")), - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(7, "7")), - Expanded = { Value = true } - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(8, "8")), - }, - new PanelGroupStarDifficulty - { - Item = new CarouselItem(new GroupDefinition(9, "9")), - Expanded = { Value = true } - }, } }; } From 76b94884b824a7665adfdc2b483c9ab67c3f507b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Apr 2025 15:31:40 +0900 Subject: [PATCH 213/281] Remove localisation support for now The plural handling doesn't cover other languages so it's a bit pointless to localise in this manner. --- osu.Game/Localisation/SongSelectStrings.cs | 15 --------------- .../Screens/SelectV2/PanelGroupStarDifficulty.cs | 7 +++---- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs index ecf68e33a8..e715ba8880 100644 --- a/osu.Game/Localisation/SongSelectStrings.cs +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -54,21 +54,6 @@ namespace osu.Game.Localisation /// public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap"); - /// - /// "Below 1 Star" - /// - public static LocalisableString BelowStar => new TranslatableString(getKey(@"below_star"), @"Below 1 Star"); - - /// - /// "1 Star" - /// - public static LocalisableString Star => new TranslatableString(getKey(@"star"), @"1 Star"); - - /// - /// "{0} Stars" - /// - public static LocalisableString Stars(int starNumber) => new TranslatableString(getKey(@"stars"), @"{0} Stars", starNumber); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 2fba25b3f0..7353fd4095 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; -using osu.Game.Localisation; namespace osu.Game.Screens.SelectV2 { @@ -147,15 +146,15 @@ namespace osu.Game.Screens.SelectV2 switch (starNumber) { case 0: - starRatingText.Text = SongSelectStrings.BelowStar; + starRatingText.Text = @"Below 1 Star"; break; case 1: - starRatingText.Text = SongSelectStrings.Star; + starRatingText.Text = @"1 Star"; break; default: - starRatingText.Text = SongSelectStrings.Stars(starNumber); + starRatingText.Text = $"{starNumber} Stars"; break; } From f79f427547de4273154e37108685e449f66be71d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Apr 2025 15:34:59 +0900 Subject: [PATCH 214/281] Remove unnecessary assert --- osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index 7353fd4095..ce46362133 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -177,8 +177,6 @@ namespace osu.Game.Screens.SelectV2 { const float duration = 500; - Debug.Assert(Item != null); - iconContainer.ResizeWidthTo(Expanded.Value ? 20f : 5f, duration, Easing.OutQuint); iconContainer.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); From 7cdbc2c20add6e8a41db071352812f8ee442a4fd Mon Sep 17 00:00:00 2001 From: Rudi Herouard Date: Sun, 13 Apr 2025 23:35:44 +0200 Subject: [PATCH 215/281] Fix "spins per minute" shows up early #31173 Make isSpinnableTime public in SpinnerRotationTracker and use it to set Tracking in OsuModSpunOut. Tracking was previously set to true, causing the "spins per minute" to appear immediately when the spinner appeared. --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- .../Skinning/Default/SpinnerRotationTracker.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 992f4d5f03..222cf4242a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var spinner = (DrawableSpinner)drawable; - spinner.RotationTracker.Tracking = true; + spinner.RotationTracker.Tracking = spinner.RotationTracker.IsSpinnableTime; // early-return if we were paused to avoid division-by-zero in the subsequent calculations. if (Precision.AlmostEquals(spinner.Clock.Rate, 0)) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 7e97f826f9..7cd1f39871 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// /// Whether currently in the correct time range to allow spinning. /// - private bool isSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current; + public bool IsSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current; protected override bool OnMouseMove(MouseMoveEvent e) { @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default lastAngle = thisAngle; } - IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; + IsSpinning.Value = IsSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// The delta angle. public void AddRotation(float delta) { - if (!isSpinnableTime) + if (!IsSpinnableTime) return; if (!rotationTransferred) From f4cb3a7fb3e4217379af0d4ab7ec44269beed718 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Apr 2025 13:54:11 +0900 Subject: [PATCH 216/281] Add support for closing chat channels with middle click Closes https://github.com/ppy/osu/issues/32797. --- .../Visual/Online/TestSceneChatOverlay.cs | 26 +++++++++++++++++++ .../Chat/ChannelList/ChannelListItem.cs | 12 +++++++++ 2 files changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index ab9ee1d8cc..d0fc66252e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -215,6 +215,32 @@ namespace osu.Game.Tests.Visual.Online }); } + [Test] + public void TestChannelCloseViaMiddleClick() + { + var testPMChannel = new Channel(testUser); + + AddStep("Show overlay", () => chatOverlay.Show()); + joinTestChannel(0); + joinChannel(testPMChannel); + AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel))); + AddStep("Middle click", () => + { + var item = getChannelListItem(testPMChannel); + InputManager.MoveMouseTo(item); + InputManager.Click(MouseButton.Middle); + }); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel)); + AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Click close button", () => + { + var item = getChannelListItem(testChannel1); + InputManager.MoveMouseTo(item); + InputManager.Click(MouseButton.Middle); + }); + AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1)); + } + [Test] public void TestChannelCloseButton() { diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 6107f130ec..3741852993 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -18,6 +18,7 @@ using osu.Game.Online.Chat; using osu.Game.Overlays.Chat.Listing; using osu.Game.Users.Drawables; using osuTK; +using osuTK.Input; namespace osu.Game.Overlays.Chat.ChannelList { @@ -160,6 +161,17 @@ namespace osu.Game.Overlays.Chat.ChannelList }; } + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Middle) + { + close?.TriggerClick(); + return true; + } + + return base.OnMouseDown(e); + } + private ChannelListItemCloseButton? createCloseButton() { if (isSelector || !CanLeave) From 80d9f742da7b9efa0f98ca7715351bcc427e8a70 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 14 Apr 2025 17:45:15 +0900 Subject: [PATCH 217/281] Combine "spinnable time" conditions --- .../Objects/Drawables/DrawableSpinner.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 8c21e6a6bc..64cedd216b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -277,13 +277,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); if (HandleUserInput) - { - bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime; - - RotationTracker.Tracking = !Result.HasResult - && correctButtonPressed() - && isValidSpinningTime; - } + RotationTracker.Tracking = RotationTracker.IsSpinnableTime && !Result.HasResult && correctButtonPressed(); if (spinningSample != null && spinnerFrequencyModulate) spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress; From f05a50f4e19fa215dfd761b9de328945fbe1bd25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Apr 2025 18:38:04 +0900 Subject: [PATCH 218/281] Rename new property to better explain visual-only usage --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs | 6 +++--- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs | 4 ++-- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 6 +++--- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs | 6 +++--- osu.Game/Beatmaps/BeatmapStatistic.cs | 7 ++----- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index e9d087929f..eadf7f42bc 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -26,21 +26,21 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Name = @"Fruits", Content = fruits.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), - Ratio = fruits / (float)sum, + BarDisplayLength = fruits / (float)sum, }, new BeatmapStatistic { Name = @"Juice Streams", Content = juiceStreams.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), - Ratio = juiceStreams / (float)sum, + BarDisplayLength = juiceStreams / (float)sum, }, new BeatmapStatistic { Name = @"Banana Showers", Content = bananaShowers.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), - Ratio = Math.Min(bananaShowers / 10f, 1), + BarDisplayLength = Math.Min(bananaShowers / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 16e1751e95..3ee1b63800 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -45,14 +45,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Name = @"Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), - Ratio = notes / (float)sum, + BarDisplayLength = notes / (float)sum, }, new BeatmapStatistic { Name = @"Hold Notes", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdNotes.ToString(), - Ratio = holdNotes / (float)sum, + BarDisplayLength = holdNotes / (float)sum, }, }; } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index cc73f2860a..2600f63ab9 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -25,21 +25,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps Name = "Circles", Content = circles.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), - Ratio = circles / (float)sum, + BarDisplayLength = circles / (float)sum, }, new BeatmapStatistic { Name = "Sliders", Content = sliders.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), - Ratio = sliders / (float)sum, + BarDisplayLength = sliders / (float)sum, }, new BeatmapStatistic { Name = @"Spinners", Content = spinners.ToString(), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), - Ratio = Math.Min(spinners / 10f, 1), + BarDisplayLength = Math.Min(spinners / 10f, 1), } }; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index ad4413d84a..e8cd05ee73 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -25,21 +25,21 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Name = @"Hits", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), - Ratio = hits / (float)sum, + BarDisplayLength = hits / (float)sum, }, new BeatmapStatistic { Name = @"Drumrolls", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumRolls.ToString(), - Ratio = drumRolls / (float)sum, + BarDisplayLength = drumRolls / (float)sum, }, new BeatmapStatistic { Name = @"Swells", CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), - Ratio = Math.Min(swells / 10f, 1), + BarDisplayLength = Math.Min(swells / 10f, 1), } }; } diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 6faf74d9c6..64e42f3f02 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -27,11 +27,8 @@ namespace osu.Game.Beatmaps public string Content; /// - /// The ratio of this statistic compared to other relevant statistics, or null if not applicable. + /// The length of a bar which visually represents this statistic's relevance in the beatmap. /// - /// - /// This is used to display a bar on top of the statistic with the given ratio. - /// - public float? Ratio; + public float? BarDisplayLength; } } From 3d47a2b5b2b94a92cbd757b705561519ad3c93d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Apr 2025 19:21:36 +0900 Subject: [PATCH 219/281] Don't include spinner types in note sums --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs | 2 +- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 2 +- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index eadf7f42bc..d43290e661 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int fruits = HitObjects.Count(s => s is Fruit); int juiceStreams = HitObjects.Count(s => s is JuiceStream); int bananaShowers = HitObjects.Count(s => s is BananaShower); - int sum = Math.Max(1, fruits + juiceStreams + bananaShowers); + int sum = Math.Max(1, fruits + juiceStreams); return new[] { diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index 2600f63ab9..d11b4aac3b 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps int circles = HitObjects.Count(c => c is HitCircle); int sliders = HitObjects.Count(s => s is Slider); int spinners = HitObjects.Count(s => s is Spinner); - int sum = Math.Max(1, circles + sliders + spinners); + int sum = Math.Max(1, circles + sliders); return new[] { diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index e8cd05ee73..5b0582ab59 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps int hits = HitObjects.Count(s => s is Hit); int drumRolls = HitObjects.Count(s => s is DrumRoll); int swells = HitObjects.Count(s => s is Swell); - int sum = Math.Max(1, hits + drumRolls + swells); + int sum = Math.Max(1, hits + drumRolls); return new[] { From 9cfba9008fc726bb785b39f29608ab3492d130a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Apr 2025 13:13:21 +0200 Subject: [PATCH 220/281] Add extra comments regarding notation --- osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs | 4 ++++ osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs | 4 ++++ osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs index 497d8a18b8..a83b61360b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -17,6 +17,10 @@ namespace osu.Game.Rulesets.Mania.Tests { private static readonly object[][] test_cases = { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + // OD = 5 test cases. // PERFECT hit window is [ -19.4ms, 19.4ms] // GREAT hit window is [ -49.0ms, 49.0ms] diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs index aca8f757f2..2303b17d96 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayStability.cs @@ -18,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Tests { private static readonly object[][] test_cases = { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + // OD = 5 test cases. // GREAT hit window is [ -50ms, 50ms] // OK hit window is [-100ms, 100ms] diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs index 4a2cd024b0..62bbebcf0b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayStability.cs @@ -17,6 +17,10 @@ namespace osu.Game.Rulesets.Taiko.Tests { private static readonly object[][] test_cases = { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + // OD = 5 test cases. // GREAT hit window is [-35ms, 35ms] // OK hit window is [-80ms, 80ms] From 82b2a92894a796b365404dd42bb78ca38b4bf356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Apr 2025 13:47:42 +0200 Subject: [PATCH 221/281] Add test cases covering correct legacy replay playback with respect to hitwindow treatment This continues on https://github.com/ppy/osu/pull/32770 via adding test cases which cover treatment of hit windows in stable in osu!, taiko, and mania. The test cases are exportable to beatmap `.osu` files and replay `.osr` files for stable crosscheck by setting `ExportLocation` on the test scene classes to a non-null path. For the most part, osu! and taiko ground truth matches previous findings - hit windows in those rulesets are floored to the nearest integer. The real "star" of this diff is mania, because: - The hit windows in mania depend on: - overall difficulty (as expected) - whether Score V2 is active - if Score V2 is not active, the hit windows also depend on whether the map was converted from another ruleset or not - Regardless of all aforementioned factors, mania hitwindows are *not symmetrical*. Due to what *appears* to be a straight-up bug, it is *not possible to achieve a MEH / 50 hit result when hitting late*. There is specific code that coerces late hits beyond 100 hit window range to full misses: https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/HitObjectManagerMania.cs#L737-L751 Note that despite the fact that I'm PRing these test cases, none of this is a promise that all of stable behaviours will be returning unchanged when I PR something to actually do something about this and the other issue of replay instability. This is just coverage, to be used for awareness of what's still broken. The extent of how much stable is going to be humored here going forward will be subject to negotiation. --- .../TestSceneLegacyReplayPlayback.cs | 470 ++++++++++++++++++ .../TestSceneLegacyReplayPlayback.cs | 118 +++++ .../TestSceneLegacyReplayPlayback.cs | 102 ++++ .../Visual/LegacyReplayPlaybackTestScene.cs | 157 ++++++ 4 files changed, 847 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs create mode 100644 osu.Game/Tests/Visual/LegacyReplayPlaybackTestScene.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs new file mode 100644 index 0000000000..acd97b92a9 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs @@ -0,0 +1,470 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene + { + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + + protected override string? ExportLocation => null; + + private static readonly object[][] score_v2_test_cases = + { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + + // Note that mania hitwindows are heavily idiosyncratic, + // and if you *think* a number here is wrong, probably double check. + + // Known issues / complexities: + // - There is a disparate set of hitwindow ranges for: score V1 non-converts, score V1 converts, and score V2 (regardless of convert) + // - It is NEVER POSSIBLE to get a MEH result when late; exceeding the OK hit windows will result in a MISS. + // Additionally, the OK hit window when late is EXCLUSIVE / OPEN rather than INCLUSIVE / CLOSED. + // Relevant stable source: https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/HitObjectManagerMania.cs#L737-L751 + // - There is also a seemingly mania-specific issue wherein key inputs registered before time instant 0 get truncated to time 0, + // which is why the beatmaps used below make sure not to cross that boundary (the note starts at t=300ms). + // This is not an issue in osu! or taiko. + // The source of this behaviour has not been investigated in detail. + + // OD = 5 test cases. + // PERFECT hit window is [ -19ms, 19ms] + // GREAT hit window is [ -49ms, 49ms] + // GOOD hit window is [ -82ms, 82ms] + // OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 5f, -18d, HitResult.Perfect }, + new object[] { 5f, -19d, HitResult.Perfect }, + new object[] { 5f, -20d, HitResult.Great }, + new object[] { 5f, -21d, HitResult.Great }, + new object[] { 5f, -48d, HitResult.Great }, + new object[] { 5f, -49d, HitResult.Great }, + new object[] { 5f, -50d, HitResult.Good }, + new object[] { 5f, -51d, HitResult.Good }, + new object[] { 5f, -81d, HitResult.Good }, + new object[] { 5f, -82d, HitResult.Good }, + new object[] { 5f, -83d, HitResult.Ok }, + new object[] { 5f, -84d, HitResult.Ok }, + new object[] { 5f, -111d, HitResult.Ok }, + new object[] { 5f, -112d, HitResult.Ok }, + new object[] { 5f, -113d, HitResult.Meh }, + new object[] { 5f, -114d, HitResult.Meh }, + new object[] { 5f, -135d, HitResult.Meh }, + new object[] { 5f, -136d, HitResult.Meh }, + new object[] { 5f, -137d, HitResult.Miss }, + new object[] { 5f, -138d, HitResult.Miss }, + new object[] { 5f, 111d, HitResult.Ok }, + new object[] { 5f, 112d, HitResult.Miss }, + new object[] { 5f, 113d, HitResult.Miss }, + new object[] { 5f, 114d, HitResult.Miss }, + new object[] { 5f, 135d, HitResult.Miss }, + new object[] { 5f, 136d, HitResult.Miss }, + new object[] { 5f, 137d, HitResult.Miss }, + new object[] { 5f, 138d, HitResult.Miss }, + + // OD = 9.3 test cases. + // PERFECT hit window is [ -14ms, 14ms] + // GREAT hit window is [ -36ms, 36ms] + // GOOD hit window is [ -69ms, 69ms] + // OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 9.3f, 13d, HitResult.Perfect }, + new object[] { 9.3f, 14d, HitResult.Perfect }, + new object[] { 9.3f, 15d, HitResult.Great }, + new object[] { 9.3f, 16d, HitResult.Great }, + new object[] { 9.3f, 35d, HitResult.Great }, + new object[] { 9.3f, 36d, HitResult.Great }, + new object[] { 9.3f, 37d, HitResult.Good }, + new object[] { 9.3f, 38d, HitResult.Good }, + new object[] { 9.3f, 68d, HitResult.Good }, + new object[] { 9.3f, 69d, HitResult.Good }, + new object[] { 9.3f, 70d, HitResult.Ok }, + new object[] { 9.3f, 71d, HitResult.Ok }, + new object[] { 9.3f, 98d, HitResult.Ok }, + new object[] { 9.3f, 99d, HitResult.Miss }, + new object[] { 9.3f, 100d, HitResult.Miss }, + new object[] { 9.3f, 101d, HitResult.Miss }, + new object[] { 9.3f, 122d, HitResult.Miss }, + new object[] { 9.3f, 123d, HitResult.Miss }, + new object[] { 9.3f, 124d, HitResult.Miss }, + new object[] { 9.3f, 125d, HitResult.Miss }, + new object[] { 9.3f, -98d, HitResult.Ok }, + new object[] { 9.3f, -99d, HitResult.Ok }, + new object[] { 9.3f, -100d, HitResult.Meh }, + new object[] { 9.3f, -101d, HitResult.Meh }, + new object[] { 9.3f, -122d, HitResult.Meh }, + new object[] { 9.3f, -123d, HitResult.Meh }, + new object[] { 9.3f, -124d, HitResult.Miss }, + new object[] { 9.3f, -125d, HitResult.Miss }, + }; + + private static readonly object[][] score_v1_non_convert_test_cases = + { + // OD = 5 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -49ms, 49ms] + // GOOD hit window is [ -82ms, 82ms] + // OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 5f, -15d, HitResult.Perfect }, + new object[] { 5f, -16d, HitResult.Perfect }, + new object[] { 5f, -17d, HitResult.Great }, + new object[] { 5f, -18d, HitResult.Great }, + new object[] { 5f, -48d, HitResult.Great }, + new object[] { 5f, -49d, HitResult.Great }, + new object[] { 5f, -50d, HitResult.Good }, + new object[] { 5f, -51d, HitResult.Good }, + new object[] { 5f, -81d, HitResult.Good }, + new object[] { 5f, -82d, HitResult.Good }, + new object[] { 5f, -83d, HitResult.Ok }, + new object[] { 5f, -84d, HitResult.Ok }, + new object[] { 5f, -111d, HitResult.Ok }, + new object[] { 5f, -112d, HitResult.Ok }, + new object[] { 5f, -113d, HitResult.Meh }, + new object[] { 5f, -114d, HitResult.Meh }, + new object[] { 5f, -135d, HitResult.Meh }, + new object[] { 5f, -136d, HitResult.Meh }, + new object[] { 5f, -137d, HitResult.Miss }, + new object[] { 5f, -138d, HitResult.Miss }, + new object[] { 5f, 111d, HitResult.Ok }, + new object[] { 5f, 112d, HitResult.Miss }, + new object[] { 5f, 113d, HitResult.Miss }, + new object[] { 5f, 114d, HitResult.Miss }, + new object[] { 5f, 135d, HitResult.Miss }, + new object[] { 5f, 136d, HitResult.Miss }, + new object[] { 5f, 137d, HitResult.Miss }, + new object[] { 5f, 138d, HitResult.Miss }, + + // OD = 9.3 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -36ms, 36ms] + // GOOD hit window is [ -69ms, 69ms] + // OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 9.3f, 15d, HitResult.Perfect }, + new object[] { 9.3f, 16d, HitResult.Perfect }, + new object[] { 9.3f, 17d, HitResult.Great }, + new object[] { 9.3f, 18d, HitResult.Great }, + new object[] { 9.3f, 35d, HitResult.Great }, + new object[] { 9.3f, 36d, HitResult.Great }, + new object[] { 9.3f, 37d, HitResult.Good }, + new object[] { 9.3f, 38d, HitResult.Good }, + new object[] { 9.3f, 68d, HitResult.Good }, + new object[] { 9.3f, 69d, HitResult.Good }, + new object[] { 9.3f, 70d, HitResult.Ok }, + new object[] { 9.3f, 71d, HitResult.Ok }, + new object[] { 9.3f, 98d, HitResult.Ok }, + new object[] { 9.3f, 99d, HitResult.Miss }, + new object[] { 9.3f, 100d, HitResult.Miss }, + new object[] { 9.3f, 101d, HitResult.Miss }, + new object[] { 9.3f, 122d, HitResult.Miss }, + new object[] { 9.3f, 123d, HitResult.Miss }, + new object[] { 9.3f, 124d, HitResult.Miss }, + new object[] { 9.3f, 125d, HitResult.Miss }, + new object[] { 9.3f, -98d, HitResult.Ok }, + new object[] { 9.3f, -99d, HitResult.Ok }, + new object[] { 9.3f, -100d, HitResult.Meh }, + new object[] { 9.3f, -101d, HitResult.Meh }, + new object[] { 9.3f, -122d, HitResult.Meh }, + new object[] { 9.3f, -123d, HitResult.Meh }, + new object[] { 9.3f, -124d, HitResult.Miss }, + new object[] { 9.3f, -125d, HitResult.Miss }, + + // OD = 3.1 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -54ms, 54ms] + // GOOD hit window is [ -87ms, 87ms] + // OK hit window is [-117ms, 117ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-141ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 3.1f, 15d, HitResult.Perfect }, + new object[] { 3.1f, 16d, HitResult.Perfect }, + new object[] { 3.1f, 17d, HitResult.Great }, + new object[] { 3.1f, 18d, HitResult.Great }, + new object[] { 3.1f, 53d, HitResult.Great }, + new object[] { 3.1f, 54d, HitResult.Great }, + new object[] { 3.1f, 55d, HitResult.Good }, + new object[] { 3.1f, 56d, HitResult.Good }, + new object[] { 3.1f, 86d, HitResult.Good }, + new object[] { 3.1f, 87d, HitResult.Good }, + new object[] { 3.1f, 88d, HitResult.Ok }, + new object[] { 3.1f, 89d, HitResult.Ok }, + new object[] { 3.1f, 116d, HitResult.Ok }, + new object[] { 3.1f, 117d, HitResult.Miss }, + new object[] { 3.1f, 118d, HitResult.Miss }, + new object[] { 3.1f, 119d, HitResult.Miss }, + new object[] { 3.1f, 140d, HitResult.Miss }, + new object[] { 3.1f, 141d, HitResult.Miss }, + new object[] { 3.1f, 142d, HitResult.Miss }, + new object[] { 3.1f, 143d, HitResult.Miss }, + new object[] { 3.1f, -116d, HitResult.Ok }, + new object[] { 3.1f, -117d, HitResult.Ok }, + new object[] { 3.1f, -118d, HitResult.Meh }, + new object[] { 3.1f, -119d, HitResult.Meh }, + new object[] { 3.1f, -140d, HitResult.Meh }, + new object[] { 3.1f, -141d, HitResult.Meh }, + new object[] { 3.1f, -142d, HitResult.Miss }, + new object[] { 3.1f, -143d, HitResult.Miss }, + }; + + private static readonly object[][] score_v1_convert_test_cases = + { + // OD = 5 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -34ms, 34ms] + // GOOD hit window is [ -67ms, 67ms] + // OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 5f, -15d, HitResult.Perfect }, + new object[] { 5f, -16d, HitResult.Perfect }, + new object[] { 5f, -17d, HitResult.Great }, + new object[] { 5f, -18d, HitResult.Great }, + new object[] { 5f, -33d, HitResult.Great }, + new object[] { 5f, -34d, HitResult.Great }, + new object[] { 5f, -35d, HitResult.Good }, + new object[] { 5f, -36d, HitResult.Good }, + new object[] { 5f, -66d, HitResult.Good }, + new object[] { 5f, -67d, HitResult.Good }, + new object[] { 5f, -68d, HitResult.Ok }, + new object[] { 5f, -69d, HitResult.Ok }, + new object[] { 5f, -96d, HitResult.Ok }, + new object[] { 5f, -97d, HitResult.Ok }, + new object[] { 5f, -98d, HitResult.Meh }, + new object[] { 5f, -99d, HitResult.Meh }, + new object[] { 5f, -120d, HitResult.Meh }, + new object[] { 5f, -121d, HitResult.Meh }, + new object[] { 5f, -122d, HitResult.Miss }, + new object[] { 5f, -123d, HitResult.Miss }, + new object[] { 5f, 96d, HitResult.Ok }, + new object[] { 5f, 97d, HitResult.Miss }, + new object[] { 5f, 98d, HitResult.Miss }, + new object[] { 5f, 99d, HitResult.Miss }, + new object[] { 5f, 120d, HitResult.Miss }, + new object[] { 5f, 121d, HitResult.Miss }, + new object[] { 5f, 122d, HitResult.Miss }, + new object[] { 5f, 123d, HitResult.Miss }, + + // OD = 3.1 test cases. + // PERFECT hit window is [ -16ms, 16ms] + // GREAT hit window is [ -47ms, 47ms] + // GOOD hit window is [ -77ms, 77ms] + // OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN! + // MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit! + new object[] { 3.1f, 15d, HitResult.Perfect }, + new object[] { 3.1f, 16d, HitResult.Perfect }, + new object[] { 3.1f, 17d, HitResult.Great }, + new object[] { 3.1f, 18d, HitResult.Great }, + new object[] { 3.1f, 46d, HitResult.Great }, + new object[] { 3.1f, 47d, HitResult.Great }, + new object[] { 3.1f, 48d, HitResult.Good }, + new object[] { 3.1f, 49d, HitResult.Good }, + new object[] { 3.1f, 76d, HitResult.Good }, + new object[] { 3.1f, 77d, HitResult.Good }, + new object[] { 3.1f, 78d, HitResult.Ok }, + new object[] { 3.1f, 79d, HitResult.Ok }, + new object[] { 3.1f, 96d, HitResult.Ok }, + new object[] { 3.1f, 97d, HitResult.Miss }, + new object[] { 3.1f, 98d, HitResult.Miss }, + new object[] { 3.1f, 99d, HitResult.Miss }, + new object[] { 3.1f, 120d, HitResult.Miss }, + new object[] { 3.1f, 121d, HitResult.Miss }, + new object[] { 3.1f, 122d, HitResult.Miss }, + new object[] { 3.1f, 123d, HitResult.Miss }, + new object[] { 3.1f, -96d, HitResult.Ok }, + new object[] { 3.1f, -97d, HitResult.Ok }, + new object[] { 3.1f, -98d, HitResult.Meh }, + new object[] { 3.1f, -99d, HitResult.Meh }, + new object[] { 3.1f, -120d, HitResult.Meh }, + new object[] { 3.1f, -121d, HitResult.Meh }, + new object[] { 3.1f, -122d, HitResult.Miss }, + new object[] { 3.1f, -123d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(score_v2_test_cases))] + public void TestHitWindowTreatmentWithScoreV2(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double note_time = 300; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = + { + new Note + { + StartTime = note_time, + Column = 0, + } + }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = overallDifficulty, + CircleSize = 1, + }, + BeatmapInfo = + { + Ruleset = new ManiaRuleset().RulesetInfo, + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + new ManiaReplayFrame(0), + new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1), + new ManiaReplayFrame(note_time + hitOffset + 20), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + Mods = [new ModScoreV2()] + } + }; + + RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + + [TestCaseSource(nameof(score_v1_non_convert_test_cases))] + public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double note_time = 300; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new ManiaBeatmap(new StageDefinition(1)) + { + HitObjects = + { + new Note + { + StartTime = note_time, + Column = 0, + } + }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = overallDifficulty, + CircleSize = 1, + }, + BeatmapInfo = + { + Ruleset = new ManiaRuleset().RulesetInfo, + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + new ManiaReplayFrame(0), + new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1), + new ManiaReplayFrame(note_time + hitOffset + 20), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + } + }; + + RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + + [TestCaseSource(nameof(score_v1_convert_test_cases))] + public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double note_time = 300; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new Beatmap + { + HitObjects = + { + new FakeCircle + { + StartTime = note_time, + } + }, + Difficulty = new BeatmapDifficulty + { + OverallDifficulty = overallDifficulty, + }, + BeatmapInfo = + { + Ruleset = new RulesetInfo { OnlineID = 0 } + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + new ManiaReplayFrame(0), + new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1), + new ManiaReplayFrame(note_time + hitOffset + 20), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + Mods = [new ManiaModKey1()], + } + }; + + RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + + private class FakeCircle : HitObject, IHasPosition + { + public float X + { + get => Position.X; + set => Position = new Vector2(value, Position.Y); + } + + public float Y + { + get => Position.Y; + set => Position = new Vector2(Position.X, value); + } + + public Vector2 Position { get; set; } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs new file mode 100644 index 0000000000..5a085fe17c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs @@ -0,0 +1,118 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene + { + protected override Ruleset CreateRuleset() => new OsuRuleset(); + + protected override string? ExportLocation => null; + + private static readonly object[][] test_cases = + { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + // Additionally, note that offsets provided in double will be rounded to the nearest integer. + + // OD = 5 test cases. + // GREAT hit window is ( -50ms, 50ms) + // OK hit window is (-100ms, 100ms) + // MEH hit window is (-150ms, 150ms) + new object[] { 5f, 48d, HitResult.Great }, + new object[] { 5f, 49d, HitResult.Great }, + new object[] { 5f, 50d, HitResult.Ok }, + new object[] { 5f, 51d, HitResult.Ok }, + new object[] { 5f, 98d, HitResult.Ok }, + new object[] { 5f, 99d, HitResult.Ok }, + new object[] { 5f, 100d, HitResult.Meh }, + new object[] { 5f, 101d, HitResult.Meh }, + new object[] { 5f, 148d, HitResult.Meh }, + new object[] { 5f, 149d, HitResult.Meh }, + new object[] { 5f, 150d, HitResult.Miss }, + new object[] { 5f, 151d, HitResult.Miss }, + + // OD = 5.7 test cases. + // GREAT hit window is ( -45ms, 45ms) + // OK hit window is ( -94ms, 94ms) + // MEH hit window is (-143ms, 143ms) + new object[] { 5.7f, 43d, HitResult.Great }, + new object[] { 5.7f, 44d, HitResult.Great }, + new object[] { 5.7f, 45d, HitResult.Ok }, + new object[] { 5.7f, 46d, HitResult.Ok }, + new object[] { 5.7f, 92d, HitResult.Ok }, + new object[] { 5.7f, 93d, HitResult.Ok }, + new object[] { 5.7f, 94d, HitResult.Meh }, + new object[] { 5.7f, 95d, HitResult.Meh }, + new object[] { 5.7f, 141d, HitResult.Meh }, + new object[] { 5.7f, 142d, HitResult.Meh }, + new object[] { 5.7f, 143d, HitResult.Miss }, + new object[] { 5.7f, 144d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double hit_circle_time = 100; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new OsuBeatmap + { + HitObjects = + { + new HitCircle + { + StartTime = hit_circle_time, + Position = OsuPlayfield.BASE_SIZE / 2 + } + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo, + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + // required for correct playback in stable + new OsuReplayFrame(0, new Vector2(256, -500)), + new OsuReplayFrame(0, new Vector2(256, -500)), + new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2), + new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton), + new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + } + }; + + RunTest($@"single circle @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs new file mode 100644 index 0000000000..4703b38e57 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs @@ -0,0 +1,102 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")] + public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene + { + protected override string? ExportLocation => null; + + protected override Ruleset? CreateRuleset() => new TaikoRuleset(); + + private static readonly object[][] test_cases = + { + // With respect to notation, + // square brackets `[]` represent *closed* or *inclusive* bounds, + // while round brackets `()` represent *open* or *exclusive* bounds. + + // OD = 5 test cases. + // GREAT hit window is (-35ms, 35ms) + // OK hit window is (-80ms, 80ms) + new object[] { 5f, -33d, HitResult.Great }, + new object[] { 5f, -34d, HitResult.Great }, + new object[] { 5f, -35d, HitResult.Ok }, + new object[] { 5f, -36d, HitResult.Ok }, + new object[] { 5f, -78d, HitResult.Ok }, + new object[] { 5f, -79d, HitResult.Ok }, + new object[] { 5f, -80d, HitResult.Miss }, + new object[] { 5f, -81d, HitResult.Miss }, + + // OD = 7.8 test cases. + // GREAT hit window is (-26ms, 26ms) + // OK hit window is (-63ms, 63ms) + new object[] { 7.8f, -24d, HitResult.Great }, + new object[] { 7.8f, -25d, HitResult.Great }, + new object[] { 7.8f, -26d, HitResult.Ok }, + new object[] { 7.8f, -27d, HitResult.Ok }, + new object[] { 7.8f, -61d, HitResult.Ok }, + new object[] { 7.8f, -62d, HitResult.Ok }, + new object[] { 7.8f, -63d, HitResult.Miss }, + new object[] { 7.8f, -64d, HitResult.Miss }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult) + { + const double hit_time = 100; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 1000 }); + var beatmap = new TaikoBeatmap + { + HitObjects = + { + new Hit + { + StartTime = hit_time, + Type = HitType.Centre, + } + }, + Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty }, + BeatmapInfo = + { + Ruleset = new TaikoRuleset().RulesetInfo, + }, + ControlPointInfo = cpi, + }; + + var replay = new Replay + { + Frames = + { + new TaikoReplayFrame(0), + new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre), + new TaikoReplayFrame(hit_time + hitOffset + 20), + } + }; + + var score = new Score + { + Replay = replay, + ScoreInfo = new ScoreInfo + { + Ruleset = CreateRuleset()!.RulesetInfo, + } + }; + + RunTest($@"single hit @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); + } + } +} diff --git a/osu.Game/Tests/Visual/LegacyReplayPlaybackTestScene.cs b/osu.Game/Tests/Visual/LegacyReplayPlaybackTestScene.cs new file mode 100644 index 0000000000..5f973d1e4e --- /dev/null +++ b/osu.Game/Tests/Visual/LegacyReplayPlaybackTestScene.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO.Legacy; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + /// + /// The goal of this abstract test class is to exercise correct playback of replays sourced from previous osu! versions. + /// Use to exercise that property. + /// + [HeadlessTest] + [TestFixture] + public abstract partial class LegacyReplayPlaybackTestScene : RateAdjustedBeatmapTestScene + { + private ReplayPlayer currentPlayer = null!; + private readonly List results = new List(); + + /// + /// This is provided as a convenience for testing behaviour against osu!stable. + /// Setting this field to a non-null path will cause beatmap files and replays used in all test cases + /// to be exported to disk so that they can be cross-checked against stable. + /// + protected abstract string? ExportLocation { get; } + + /// + /// Encodes the supplied , decodes the result of encoding, runs the result of decoding against the supplied , + /// and checks that the judgement results recorded still match . + /// If is set, exports both the beatmap and the replay to said location. + /// + protected void RunTest(string beatmapName, IBeatmap beatmap, string replayName, Score originalScore, IEnumerable expectedResults) + { + IBeatmap playableBeatmap = null!; + MemoryStream beatmapStream = new MemoryStream(); + MemoryStream scoreStream = new MemoryStream(); + Score decodedScore = null!; + + AddStep(@"set up beatmap", () => + { + beatmap.Metadata.Title = beatmapName; + Beatmap.Value = CreateWorkingBeatmap(beatmap); + Ruleset.Value = CreateRuleset()!.RulesetInfo; + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + + var beatmapEncoder = new LegacyBeatmapEncoder(beatmap, null); + + using (var writer = new StreamWriter(beatmapStream, Encoding.UTF8, leaveOpen: true)) + beatmapEncoder.Encode(writer); + + beatmapStream.Seek(0, SeekOrigin.Begin); + playableBeatmap.BeatmapInfo.MD5Hash = beatmapStream.ComputeMD5Hash(); + }); + + AddStep(@"encode score", () => + { + originalScore.ScoreInfo.BeatmapInfo = playableBeatmap.BeatmapInfo; + var encoder = new LegacyScoreEncoder(originalScore, playableBeatmap); + encoder.Encode(scoreStream, leaveOpen: true); + + // `LegacyScoreEncoder` hardcodes a replay version that belongs to lazer. + // here we want to simulate a stable replay, which should have the classic mod attached etc. + // to that end, we do a post-encode step to specify a stable-like replay version. + scoreStream.Position = 1; + + using (var sw = new SerializationWriter(scoreStream, leaveOpen: true)) + { + const int version = 20250414; + sw.Write(version); + } + + scoreStream.Position = 0; + }); + + if (ExportLocation != null) + { + AddStep("export beatmap", () => + { + using var stream = File.Open(Path.Combine(ExportLocation, $"{beatmapName}.osu"), FileMode.Create); + beatmapStream.CopyTo(stream); + beatmapStream.Position = 0; + }); + + AddStep("export score", () => + { + using var stream = File.Open(Path.Combine(ExportLocation, $@"{replayName}.osr"), FileMode.Create); + scoreStream.CopyTo(stream); + scoreStream.Position = 0; + }); + } + + AddStep(@"decode score", () => + { + using (scoreStream) + { + scoreStream.Position = 0; + decodedScore = new TestScoreDecoder(Beatmap.Value, Ruleset.Value).Parse(scoreStream); + } + }); + + AddAssert(@"classic mod present", () => decodedScore.ScoreInfo.Mods.Any(mod => mod is ModClassic)); + AddStep(@"push player", () => pushNewPlayer(decodedScore)); + + AddUntilStep(@"Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddAssert(@"classic mod present", () => currentPlayer.GameplayState.Mods.Any(mod => mod is ModClassic)); + AddUntilStep(@"Wait for completion", () => currentPlayer.GameplayState.HasCompleted); + AddAssert(@"judgement results after encode are correct", () => results.Select(r => r.Type), () => Is.EquivalentTo(expectedResults)); + } + + private void pushNewPlayer(Score score) + { + var player = new ReplayPlayer(score); + SelectedMods.Value = score.ScoreInfo.Mods; + player.OnLoadComplete += _ => + { + player.GameplayState.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == player) + results.Add(result); + }; + }; + LoadScreen(currentPlayer = player); + results.Clear(); + } + + private class TestScoreDecoder : LegacyScoreDecoder + { + private readonly WorkingBeatmap beatmap; + private readonly Ruleset ruleset; + + public TestScoreDecoder(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + this.beatmap = beatmap; + this.ruleset = ruleset.CreateInstance(); + } + + protected override Ruleset GetRuleset(int rulesetId) => ruleset; + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmap; + } + } +} From 8273583fd07e42319d8febeeffbc61ff6faaed3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 08:35:35 +0200 Subject: [PATCH 222/281] Fix code quality once more --- .../TestSceneLegacyReplayPlayback.cs | 6 +++--- .../TestSceneLegacyReplayPlayback.cs | 2 +- .../TestSceneLegacyReplayPlayback.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs index acd97b92a9..ea66386c9a 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneLegacyReplayPlayback.cs @@ -341,7 +341,7 @@ namespace osu.Game.Rulesets.Mania.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, Mods = [new ModScoreV2()] } }; @@ -393,7 +393,7 @@ namespace osu.Game.Rulesets.Mania.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, } }; @@ -442,7 +442,7 @@ namespace osu.Game.Rulesets.Mania.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, Mods = [new ManiaModKey1()], } }; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs index 5a085fe17c..c22255bbdf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyReplayPlayback.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, } }; diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs index 4703b38e57..459312f2b4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneLegacyReplayPlayback.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string? ExportLocation => null; - protected override Ruleset? CreateRuleset() => new TaikoRuleset(); + protected override Ruleset CreateRuleset() => new TaikoRuleset(); private static readonly object[][] test_cases = { @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Replay = replay, ScoreInfo = new ScoreInfo { - Ruleset = CreateRuleset()!.RulesetInfo, + Ruleset = CreateRuleset().RulesetInfo, } }; From e8af0dabea0aab5e171f66308080b3f6ae0ff9d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 15:53:20 +0900 Subject: [PATCH 223/281] Fix thread safety when calling `BeatmapStore.GetBeatmapSets` While usually we'd handle this locally by moving bind operations to `LoadComponent`, this component was explicitly made to be used in asynchronous scenarios (to allow cases like song select to coexist with realm without adding huge compliexities to the classes locally). So I think it makes sense to hide this as an implementation detail. The locked segments should all be quite fast to run so I do not see a performance issue with lock contention here. --- .../Database/RealmDetachedBeatmapStore.cs | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/osu.Game/Database/RealmDetachedBeatmapStore.cs b/osu.Game/Database/RealmDetachedBeatmapStore.cs index b05e07ef31..6954bb320a 100644 --- a/osu.Game/Database/RealmDetachedBeatmapStore.cs +++ b/osu.Game/Database/RealmDetachedBeatmapStore.cs @@ -30,7 +30,8 @@ namespace osu.Game.Database public override IBindableList GetBeatmapSets(CancellationToken? cancellationToken) { loaded.Wait(cancellationToken ?? CancellationToken.None); - return detachedBeatmapSets.GetBoundCopy(); + lock (detachedBeatmapSets) + return detachedBeatmapSets.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -65,8 +66,11 @@ namespace osu.Game.Database { var detached = frozenSets.Detach(); - detachedBeatmapSets.Clear(); - detachedBeatmapSets.AddRange(detached); + lock (detachedBeatmapSets) + { + detachedBeatmapSets.Clear(); + detachedBeatmapSets.AddRange(detached); + } }); } finally @@ -116,22 +120,28 @@ namespace osu.Game.Database if (!loaded.IsSet) return; - // If this ever leads to performance issues, we could dequeue a limited number of operations per update frame. - while (pendingOperations.TryDequeue(out var op)) + if (pendingOperations.Count == 0) + return; + + lock (detachedBeatmapSets) { - switch (op.Type) + // If this ever leads to performance issues, we could dequeue a limited number of operations per update frame. + while (pendingOperations.TryDequeue(out var op)) { - case OperationType.Insert: - detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!); - break; + switch (op.Type) + { + case OperationType.Insert: + detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!); + break; - case OperationType.Update: - detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! }); - break; + case OperationType.Update: + detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! }); + break; - case OperationType.Remove: - detachedBeatmapSets.RemoveAt(op.Index); - break; + case OperationType.Remove: + detachedBeatmapSets.RemoveAt(op.Index); + break; + } } } } From 64b9d4642adb42db7b619f76ecf5c028d9d1c3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 10:27:05 +0200 Subject: [PATCH 224/281] Add failing test --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index ebeba23123..45381b3e02 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -115,6 +115,27 @@ namespace osu.Game.Tests.Visual.SongSelect checkDisplayedCount(0); } + [Test] + public void TestLocalScoresDisplayWorksWhenStartingOffline() + { + BeatmapInfo beatmapInfo = null!; + + AddStep("Log out", () => API.Logout()); + AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local); + + AddStep(@"Set beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + leaderboard.BeatmapInfo = beatmapInfo; + }); + + clearScores(); + importMoreScores(() => beatmapInfo); + checkDisplayedCount(10); + } + [Test] public void TestLocalScoresDisplayOnBeatmapEdit() { From cf2f6d7233a1a12f44024656db5c482068e8dc15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 10:27:33 +0200 Subject: [PATCH 225/281] Fix local leaderboards not showing when starting game offline Broke in https://github.com/ppy/osu/pull/32494. Oops. --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index c52cd61c42..2896e7eab4 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Select.Leaderboards var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - if (!api.IsLoggedIn) + if (!api.IsLoggedIn && IsOnlineScope) { SetErrorState(LeaderboardState.NotLoggedIn); return null; From 7a8e96f3223618315669120de68163e21bbf384b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 16:54:01 +0900 Subject: [PATCH 226/281] Change global shear definition to be a `Vector2` Saves having this defined in 20+ places. If we ever make any changes to shear, it's 100% going to need to be applied to every usage (there will never be a case of multiple different shears in the game). Also fixes a mismatching definition in `ShearedNub`. --- .../SongSelectV2/TestSceneLeaderboardScore.cs | 4 ++-- .../Graphics/UserInterface/DialogButton.cs | 4 ++-- .../Graphics/UserInterface/ShearedButton.cs | 9 +++------ osu.Game/Graphics/UserInterface/ShearedNub.cs | 4 +--- .../UserInterface/ShearedSearchTextBox.cs | 4 ++-- .../UserInterface/ShearedSliderBar.cs | 6 +++--- .../UserInterfaceV2/ShearedDropdown.cs | 20 ++++++------------- osu.Game/OsuGame.cs | 2 +- .../Overlays/Mods/BeatmapAttributesDisplay.cs | 15 ++++++-------- osu.Game/Overlays/Mods/ModColumn.cs | 2 +- .../Mods/ModFooterInformationDisplay.cs | 2 +- osu.Game/Overlays/Mods/ModPanel.cs | 2 +- osu.Game/Overlays/Mods/ModSelectColumn.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- osu.Game/Overlays/Mods/ModSelectPanel.cs | 9 ++++----- .../Mods/RankingInformationDisplay.cs | 7 +++---- osu.Game/Screens/Footer/ScreenFooterButton.cs | 10 +++------- .../DailyChallenge/DailyChallengeIntro.cs | 18 ++++++++--------- .../Select/Options/BeatmapOptionsButton.cs | 2 +- .../SelectV2/Footer/ScreenFooterButtonMods.cs | 12 +++++------ .../Leaderboards/LeaderboardScoreV2.cs | 20 +++++++++---------- .../SelectV2/UpdateBeatmapSetButton.cs | 4 ++-- 22 files changed, 72 insertions(+), 94 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index 26d39c9203..08c0c92285 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 2f), - Shear = new Vector2(OsuGame.SHEAR, 0) + Shear = OsuGame.SHEAR, }, drawWidthText = new OsuSpriteText(), }; @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(0f, 2f), - Shear = new Vector2(OsuGame.SHEAR, 0) + Shear = OsuGame.SHEAR, }, drawWidthText = new OsuSpriteText(), }; diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index c39f41bf72..423d9637b8 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface Radius = 5, }, Colour = ButtonColour, - Shear = new Vector2(0.2f, 0), + Shear = OsuGame.SHEAR, Children = new Drawable[] { new Box @@ -149,7 +149,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Both, TriangleScale = 4, ColourDark = OsuColour.Gray(0.88f), - Shear = new Vector2(-0.2f, 0), + Shear = -OsuGame.SHEAR, ClampAxes = Axes.Y }, }, diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs index 87d269ccd4..a059490aa8 100644 --- a/osu.Game/Graphics/UserInterface/ShearedButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs @@ -11,7 +11,6 @@ using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osuTK; namespace osu.Game.Graphics.UserInterface { @@ -66,8 +65,6 @@ namespace osu.Game.Graphics.UserInterface private readonly Box background; private readonly OsuSpriteText text; - private const float shear = OsuGame.SHEAR; - private Colour4? darkerColour; private Colour4? lighterColour; private Colour4? textColour; @@ -91,10 +88,10 @@ namespace osu.Game.Graphics.UserInterface public ShearedButton(float? width = null, float height = DEFAULT_HEIGHT) { Height = height; - Padding = new MarginPadding { Horizontal = shear * height }; + Padding = new MarginPadding { Horizontal = OsuGame.SHEAR.X * height }; Content.CornerRadius = CORNER_RADIUS; - Content.Shear = new Vector2(shear, 0); + Content.Shear = OsuGame.SHEAR; Content.Masking = true; Content.Anchor = Content.Origin = Anchor.Centre; @@ -117,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, Child = text = new OsuSpriteText { Font = OsuFont.TorusAlternate.With(size: 17), diff --git a/osu.Game/Graphics/UserInterface/ShearedNub.cs b/osu.Game/Graphics/UserInterface/ShearedNub.cs index 7485f68525..17b50b5d58 100644 --- a/osu.Game/Graphics/UserInterface/ShearedNub.cs +++ b/osu.Game/Graphics/UserInterface/ShearedNub.cs @@ -26,8 +26,6 @@ namespace osu.Game.Graphics.UserInterface public const int HEIGHT = 30; public const float EXPANDED_SIZE = 50; - public static readonly Vector2 SHEAR = new Vector2(0.15f, 0); - private readonly Box fill; private readonly Container main; @@ -40,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface Size = new Vector2(EXPANDED_SIZE, HEIGHT); InternalChild = main = new Container { - Shear = SHEAR, + Shear = OsuGame.SHEAR, BorderColour = Colour4.White, BorderThickness = BORDER_WIDTH, Masking = true, diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index c6565726b5..f5fbb3411f 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -52,7 +52,7 @@ namespace osu.Game.Graphics.UserInterface public ShearedSearchTextBox() { Height = 42; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; Masking = true; CornerRadius = corner_radius; @@ -115,7 +115,7 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = CommonStrings.InputSearch; CornerRadius = corner_radius; - TextContainer.Shear = new Vector2(-OsuGame.SHEAR, 0); + TextContainer.Shear = -OsuGame.SHEAR; } protected override SpriteText CreatePlaceholder() => new SearchPlaceholder(); diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index a36b9c7a4c..e7b57f5c9e 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -58,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface public ShearedSliderBar() { - Shear = SHEAR; + Shear = OsuGame.SHEAR; Height = HEIGHT; RangePadding = EXPANDED_SIZE / 2; Children = new Drawable[] @@ -98,11 +98,11 @@ namespace osu.Game.Graphics.UserInterface }, nubContainer = new Container { - Shear = -SHEAR, + Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.Both, Child = Nub = new ShearedNub { - X = -SHEAR.X * HEIGHT / 2f, + X = -OsuGame.SHEAR.X * HEIGHT / 2f, Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index 0b9c5f294c..609f77dd7e 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -62,8 +62,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - public new MarginPadding Padding { get => base.Padding; @@ -72,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdownMenu() { - Shear = shear; + Shear = OsuGame.SHEAR; Margin = new MarginPadding { Top = 5f }; } @@ -84,12 +82,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - public ShearedMenuItem(MenuItem item) : base(item) { - Foreground.Shear = -shear; + Foreground.Shear = -OsuGame.SHEAR; } } } @@ -125,14 +121,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdown Dropdown = null!; private ShearedDropdownSearchBar searchBar = null!; - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; public ShearedDropdownHeader() { - Shear = shear; + Shear = OsuGame.SHEAR; CornerRadius = corner_radius; Masking = true; @@ -167,7 +161,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f }, Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), - Shear = -shear, + Shear = -OsuGame.SHEAR, }, }, }, @@ -178,7 +172,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 10f }, - Shear = -shear, + Shear = -OsuGame.SHEAR, Children = new Drawable[] { valueText = new TruncatingSpriteText @@ -286,12 +280,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 private partial class DropdownSearchTextBox : OsuTextBox { - private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); - [BackgroundDependencyLoader] private void load(OverlayColourProvider? colourProvider) { - TextContainer.Shear = -shear; + TextContainer.Shear = -OsuGame.SHEAR; BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0c75a4106a..70a324cd8e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -102,7 +102,7 @@ namespace osu.Game /// /// A common shear factor applied to most components of the game. /// - public const float SHEAR = 0.2f; + public static readonly Vector2 SHEAR = new Vector2(0.2f, 0); public Toolbar Toolbar { get; private set; } diff --git a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs index dedd1e336e..3cefa07cfa 100644 --- a/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs +++ b/osu.Game/Overlays/Mods/BeatmapAttributesDisplay.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Utils; -using osuTK; namespace osu.Game.Overlays.Mods { @@ -66,21 +65,19 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { - const float shear = OsuGame.SHEAR; - LeftContent.AddRange(new Drawable[] { starRatingDisplay = new StarRatingDisplay(default, animated: true) { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, }, bpmDisplay = new BPMDisplay { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Shear = new Vector2(-shear, 0), + Shear = -OsuGame.SHEAR, AutoSizeAxes = Axes.Y, Width = 75, } @@ -89,10 +86,10 @@ namespace osu.Game.Overlays.Mods RightContent.Alpha = 0; RightContent.AddRange(new Drawable[] { - circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), }, - drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), }, - overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), }, - approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), }, + circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = -OsuGame.SHEAR, }, + drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = -OsuGame.SHEAR, }, + overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = -OsuGame.SHEAR, }, + approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = -OsuGame.SHEAR, }, }); } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 326394a207..7d2ce54074 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0) + Shear = -OsuGame.SHEAR }); ItemsFlow.Padding = new MarginPadding { diff --git a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs index 6665a3b8dc..db42200775 100644 --- a/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/ModFooterInformationDisplay.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, AutoSizeAxes = Axes.X, Height = ShearedButton.DEFAULT_HEIGHT, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, CornerRadius = ShearedButton.CORNER_RADIUS, BorderThickness = ShearedButton.BORDER_THICKNESS, Masking = true, diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index b85904f22b..df72692f48 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.Centre, Origin = Anchor.Centre, Active = { BindTarget = Active }, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) }; } diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index 8a499a391c..92c75e3494 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -70,7 +70,7 @@ namespace osu.Game.Overlays.Mods { Width = WIDTH; RelativeSizeAxes = Axes.Y; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; InternalChildren = new Drawable[] { @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, Height = header_height, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Velocity = 0.7f, ClampAxes = Axes.Y }, @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.Y, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Padding = new MarginPadding { Horizontal = 17, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d36092ebed..9ba3b3774f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -168,7 +168,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Direction = FillDirection.Horizontal, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Margin = new MarginPadding { Horizontal = 70 }, @@ -726,7 +726,7 @@ namespace osu.Game.Overlays.Mods // DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear, // so we have to manually compensate. var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent); - var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR, 0), ScrollContent); + var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * OsuGame.SHEAR.X, 0), ScrollContent); bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound) && Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X); diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 284356f37e..6d48576742 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -20,7 +20,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -87,7 +86,7 @@ namespace osu.Game.Overlays.Mods Content.CornerRadius = CORNER_RADIUS; Content.BorderThickness = 2; - Shear = new Vector2(OsuGame.SHEAR, 0); + Shear = OsuGame.SHEAR; Children = new Drawable[] { @@ -128,10 +127,10 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Margin = new MarginPadding { - Left = -18 * OsuGame.SHEAR + Left = -18 * OsuGame.SHEAR.X }, ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. }, @@ -139,7 +138,7 @@ namespace osu.Game.Overlays.Mods { Font = OsuFont.Default.With(size: 12), RelativeSizeAxes = Axes.X, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`. } } diff --git a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs index 75a8f289d8..11c963f616 100644 --- a/osu.Game/Overlays/Mods/RankingInformationDisplay.cs +++ b/osu.Game/Overlays/Mods/RankingInformationDisplay.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; -using osuTK; namespace osu.Game.Overlays.Mods { @@ -52,7 +51,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, RelativeSizeAxes = Axes.Both, - Shear = new Vector2(OsuGame.SHEAR, 0), + Shear = OsuGame.SHEAR, CornerRadius = ShearedButton.CORNER_RADIUS, Masking = true, Children = new Drawable[] @@ -79,7 +78,7 @@ namespace osu.Game.Overlays.Mods { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) } } @@ -94,7 +93,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.Centre, Child = counter = new EffectCounter { - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Anchor = Anchor.Centre, Origin = Anchor.Centre, Current = { BindTarget = ModMultiplier } diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 6515203ca0..5e96eadfea 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -25,16 +25,12 @@ namespace osu.Game.Screens.Footer { public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler { - private const float shear = OsuGame.SHEAR; - protected const int CORNER_RADIUS = 10; protected const int BUTTON_HEIGHT = 75; protected const int BUTTON_WIDTH = 116; public Bindable OverlayState = new Bindable(); - protected static readonly Vector2 BUTTON_SHEAR = new Vector2(shear, 0); - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -89,7 +85,7 @@ namespace osu.Game.Screens.Footer Colour = Colour4.Black.Opacity(0.25f), Offset = new Vector2(0, 2), }, - Shear = BUTTON_SHEAR, + Shear = OsuGame.SHEAR, Masking = true, CornerRadius = CORNER_RADIUS, RelativeSizeAxes = Axes.Both, @@ -108,7 +104,7 @@ namespace osu.Game.Screens.Footer { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { @@ -135,7 +131,7 @@ namespace osu.Game.Screens.Footer }, new Container { - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Anchor = Anchor.BottomCentre, Origin = Anchor.Centre, Y = -CORNER_RADIUS, diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs index 5b423fbc6d..3ec9217aa4 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeIntro.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = new Vector2(OsuGame.SHEAR, 0f), + Shear = OsuGame.SHEAR, Children = new Drawable[] { titleContainer = new Container @@ -147,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, Text = "Today's Challenge", Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), }, } @@ -173,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, Text = room.Name.Split(':', StringSplitOptions.TrimEntries).Last(), Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f }, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate), }, } @@ -246,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, MaxWidth = horizontal_info_size, Text = beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false), Padding = new MarginPadding { Horizontal = 5f }, @@ -257,7 +257,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Text = $"Difficulty: {beatmap.DifficultyName}", Font = OsuFont.GetFont(size: 20, italics: true), MaxWidth = horizontal_info_size, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, @@ -266,13 +266,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Text = $"by {beatmap.Metadata.Author.Username}", Font = OsuFont.GetFont(size: 16, italics: true), MaxWidth = horizontal_info_size, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, starRatingDisplay = new StarRatingDisplay(default) { - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Margin = new MarginPadding(5), Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -301,7 +301,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, Current = { Value = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray() @@ -329,7 +329,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Origin = Anchor.Centre, FillMode = FillMode.Fit, Scale = new Vector2(1.2f), - Shear = new Vector2(-OsuGame.SHEAR, 0f), + Shear = -OsuGame.SHEAR, }, c => { beatmapBackground.Add(c); diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 045a518525..572b2427b1 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Select.Options Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Shear = new Vector2(0.2f, 0f), + Shear = OsuGame.SHEAR, Masking = true, EdgeEffect = new EdgeEffectParameters { diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 869aef1470..61d69ae197 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f, Depth = float.MaxValue, Origin = Anchor.BottomLeft, - Shear = BUTTON_SHEAR, + Shear = OsuGame.SHEAR, CornerRadius = CORNER_RADIUS, Size = new Vector2(BUTTON_WIDTH, bar_height), Masking = true, @@ -108,7 +108,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, UseFullGlyphHeight = false, Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold) } @@ -130,7 +130,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Scale = new Vector2(0.5f), Current = { BindTarget = Current }, ExpansionMode = ExpansionMode.AlwaysContracted, @@ -139,7 +139,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold), Mods = { BindTarget = Current }, } @@ -305,7 +305,7 @@ namespace osu.Game.Screens.SelectV2.Footer Y = -5f; Depth = float.MaxValue; Origin = Anchor.BottomLeft; - Shear = BUTTON_SHEAR; + Shear = OsuGame.SHEAR; CornerRadius = CORNER_RADIUS; AutoSizeAxes = Axes.X; Height = bar_height; @@ -329,7 +329,7 @@ namespace osu.Game.Screens.SelectV2.Footer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Shear = -BUTTON_SHEAR, + Shear = -OsuGame.SHEAR, Text = ModSelectOverlayStrings.Unranked.ToUpper(), Margin = new MarginPadding { Horizontal = 15 }, UseFullGlyphHeight = false, diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index 16599a2080..0b7b2ebbc1 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards this.score = score; this.sheared = sheared; - Shear = new Vector2(sheared ? OsuGame.SHEAR : 0, 0); + Shear = sheared ? OsuGame.SHEAR : Vector2.Zero; RelativeSizeAxes = Axes.X; Height = height; } @@ -255,7 +255,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { RelativeSizeAxes = Axes.Both, User = score.User, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), @@ -286,7 +286,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.1f), - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, RelativeSizeAxes = Axes.Both, }) { @@ -326,7 +326,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { flagBadgeAndDateContainer = new FillFlowContainer { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Direction = FillDirection.Horizontal, Spacing = new Vector2(5), AutoSizeAxes = Axes.Both, @@ -356,7 +356,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards nameLabel = new TruncatingSpriteText { RelativeSizeAxes = Axes.X, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Text = user.Username, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) } @@ -372,7 +372,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Name = @"Statistics container", Padding = new MarginPadding { Right = 40 }, Spacing = new Vector2(25, 0), - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, @@ -430,7 +430,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards }, RankContainer = new Container { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, @@ -488,7 +488,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Anchor = Anchor.TopRight, Origin = Anchor.TopRight, UseFullGlyphHeight = false, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), }, @@ -496,7 +496,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(2f, 0f), @@ -704,7 +704,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards Child = new OsuSpriteText { - Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs index e2c841f88a..ac7e3856ac 100644 --- a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs +++ b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.SelectV2 Content.Anchor = Anchor.Centre; Content.Origin = Anchor.Centre; - Content.Shear = new Vector2(OsuGame.SHEAR, 0); + Content.Shear = OsuGame.SHEAR; Content.AddRange(new Drawable[] { @@ -87,7 +87,7 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(4), - Shear = new Vector2(-OsuGame.SHEAR, 0), + Shear = -OsuGame.SHEAR, Children = new Drawable[] { new Container From 0aff50fbf5eb4284823f3dfcdc92b34601a9ba11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:23:14 +0900 Subject: [PATCH 227/281] Rename song select v2 classes and namespaces This aims to bring some conformity to naming to make it easier to understand component structure for new components. Renames are pulled out of the song select v2 changes and are more relevant there due to many new classes being added. - `V2` suffix is dropped, with v2 components being moved to a relevant V2 namespace. - Related classes have a prefix of the area they are used. - Experimenting with using partial/nested classes in the song select v2 implementation. Not committing to this yet but want to see how it plays out. - Moved base carousel components to a generic namespace to avoid confusion with actual beatmap carousel implementation. --- .../DailyChallenge/TestSceneDailyChallenge.cs | 4 +- .../BeatmapCarouselTestScene.cs} | 9 +- .../TestSceneBeatmapCarousel.cs} | 4 +- ...TestSceneBeatmapCarouselArtistGrouping.cs} | 4 +- ...SceneBeatmapCarouselDifficultyGrouping.cs} | 5 +- .../TestSceneBeatmapCarouselNoGrouping.cs} | 5 +- .../TestSceneBeatmapCarouselScrolling.cs} | 4 +- .../TestSceneFooterButtonMods.cs} | 14 +- .../SongSelectV2/TestSceneLeaderboardScore.cs | 10 +- ...cultyPanel.cs => TestScenePanelBeatmap.cs} | 5 +- ....cs => TestScenePanelBeatmapStandalone.cs} | 5 +- ...V2GroupPanel.cs => TestScenePanelGroup.cs} | 5 +- ...uselV2SetPanel.cs => TestScenePanelSet.cs} | 5 +- ...s => TestScenePanelUpdateBeatmapButton.cs} | 6 +- .../TestSceneScreenFooter.cs | 10 +- .../SongSelectV2/TestSceneSongSelect.cs | 6 +- .../Carousel}/Carousel.cs | 2 +- .../Carousel}/CarouselItem.cs | 2 +- .../Carousel}/ICarouselFilter.cs | 2 +- .../Carousel}/ICarouselPanel.cs | 2 +- .../DailyChallengeLeaderboard.cs | 10 +- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + .../SelectV2/BeatmapCarouselFilterGrouping.cs | 1 + .../SelectV2/BeatmapCarouselFilterSorting.cs | 1 + ...dScoreV2.cs => BeatmapLeaderboardScore.cs} | 6 +- .../SelectV2/Footer/BeatmapOptionsPopover.cs | 195 ----------------- ...ooterButtonMods.cs => FooterButtonMods.cs} | 6 +- ...uttonOptions.cs => FooterButtonOptions.cs} | 7 +- .../SelectV2/FooterButtonOptions_Popover.cs | 198 ++++++++++++++++++ ...rButtonRandom.cs => FooterButtonRandom.cs} | 4 +- .../SelectV2/{PanelBase.cs => Panel.cs} | 3 +- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 11 +- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 11 +- .../SelectV2/PanelBeatmapStandalone.cs | 15 +- osu.Game/Screens/SelectV2/PanelGroup.cs | 3 +- .../SelectV2/PanelGroupStarDifficulty.cs | 2 +- ...pLocalRank.cs => PanelLocalRankDisplay.cs} | 4 +- ...nelBackground.cs => PanelSetBackground.cs} | 2 +- ...tButton.cs => PanelUpdateBeatmapButton.cs} | 4 +- osu.Game/Screens/SelectV2/SongSelect.cs | 7 +- 40 files changed, 308 insertions(+), 292 deletions(-) rename osu.Game.Tests/Visual/{SongSelect/BeatmapCarouselV2TestScene.cs => SongSelectV2/BeatmapCarouselTestScene.cs} (97%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2.cs => SongSelectV2/TestSceneBeatmapCarousel.cs} (95%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs => SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs} (98%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs => SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs} (97%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs => SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs} (97%) rename osu.Game.Tests/Visual/{SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs => SongSelectV2/TestSceneBeatmapCarouselScrolling.cs} (94%) rename osu.Game.Tests/Visual/{UserInterface/TestSceneScreenFooterButtonMods.cs => SongSelectV2/TestSceneFooterButtonMods.cs} (92%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselV2DifficultyPanel.cs => TestScenePanelBeatmap.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselV2StandalonePanel.cs => TestScenePanelBeatmapStandalone.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselV2GroupPanel.cs => TestScenePanelGroup.cs} (96%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneBeatmapCarouselV2SetPanel.cs => TestScenePanelSet.cs} (95%) rename osu.Game.Tests/Visual/SongSelectV2/{TestSceneUpdateBeatmapSetButtonV2.cs => TestScenePanelUpdateBeatmapButton.cs} (90%) rename osu.Game.Tests/Visual/{UserInterface => SongSelectV2}/TestSceneScreenFooter.cs (98%) rename osu.Game/{Screens/SelectV2 => Graphics/Carousel}/Carousel.cs (99%) rename osu.Game/{Screens/SelectV2 => Graphics/Carousel}/CarouselItem.cs (98%) rename osu.Game/{Screens/SelectV2 => Graphics/Carousel}/ICarouselFilter.cs (95%) rename osu.Game/{Screens/SelectV2 => Graphics/Carousel}/ICarouselPanel.cs (97%) rename osu.Game/Screens/SelectV2/{Leaderboards/LeaderboardScoreV2.cs => BeatmapLeaderboardScore.cs} (99%) delete mode 100644 osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs rename osu.Game/Screens/SelectV2/{Footer/ScreenFooterButtonMods.cs => FooterButtonMods.cs} (98%) rename osu.Game/Screens/SelectV2/{Footer/ScreenFooterButtonOptions.cs => FooterButtonOptions.cs} (76%) create mode 100644 osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs rename osu.Game/Screens/SelectV2/{Footer/ScreenFooterButtonRandom.cs => FooterButtonRandom.cs} (97%) rename osu.Game/Screens/SelectV2/{PanelBase.cs => Panel.cs} (98%) rename osu.Game/Screens/SelectV2/{TopLocalRank.cs => PanelLocalRankDisplay.cs} (96%) rename osu.Game/Screens/SelectV2/{BeatmapSetPanelBackground.cs => PanelSetBackground.cs} (97%) rename osu.Game/Screens/SelectV2/{UpdateBeatmapSetButton.cs => PanelUpdateBeatmapButton.cs} (98%) diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index 185ebc1d39..f1422b4654 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -15,7 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.Metadata; using osu.Game.Tests.Visual.OnlinePlay; @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddStep("force transforms to finish", () => FinishTransforms(true)); AddStep("right click second score", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); + InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); InputManager.Click(MouseButton.Right); }); AddAssert("use these mods not present", diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs rename to osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index f2faeab1c4..28a0948696 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -16,6 +16,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Screens.Select; @@ -27,9 +28,9 @@ using osuTK.Graphics; using osuTK.Input; using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { - public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene + public abstract partial class BeatmapCarouselTestScene : OsuManualInputManagerTestScene { protected readonly BindableList BeatmapSets = new BindableList(); @@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.SongSelect private int beatmapCount; - protected BeatmapCarouselV2TestScene() + protected BeatmapCarouselTestScene() { store = new TestBeatmapStore { @@ -191,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .OrderBy(p => p.Y) .ElementAt(index) - .ChildrenOfType().Single() + .ChildrenOfType().Single() .TriggerClick(); }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 30ca26ce68..5fd921645b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -10,13 +10,13 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { /// /// Covers common steps which can be used for manual testing. /// [TestFixture] - public partial class TestSceneBeatmapCarouselV2 : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarousel : BeatmapCarouselTestScene { [Test] [Explicit] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs similarity index 98% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs index c378871eac..f0caa796b6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselArtistGrouping.cs @@ -9,10 +9,10 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2ArtistGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselArtistGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs index 239c693ee1..a4cdf8abcb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselDifficultyGrouping.cs @@ -5,15 +5,16 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2DifficultyGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselDifficultyGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs similarity index 97% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index b4048a5355..ac02d7a3a9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -5,16 +5,17 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; using osuTK; using osuTK.Input; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2NoGrouping : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselNoGrouping : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs similarity index 94% rename from osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs index 890e1dd6e3..da3fc98c19 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselScrolling.cs @@ -8,10 +8,10 @@ using osu.Framework.Testing; using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.SongSelect +namespace osu.Game.Tests.Visual.SongSelectV2 { [TestFixture] - public partial class TestSceneBeatmapCarouselV2Scrolling : BeatmapCarouselV2TestScene + public partial class TestSceneBeatmapCarouselScrolling : BeatmapCarouselTestScene { [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs similarity index 92% rename from osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs index e86f83ee15..5c2c6eaf1d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneFooterButtonMods.cs @@ -13,19 +13,19 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; using osu.Game.Utils; -namespace osu.Game.Tests.Visual.UserInterface +namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneScreenFooterButtonMods : OsuTestScene + public partial class TestSceneFooterButtonMods : OsuTestScene { private readonly TestScreenFooterButtonMods footerButtonMods; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - public TestSceneScreenFooterButtonMods() + public TestSceneFooterButtonMods() { Add(footerButtonMods = new TestScreenFooterButtonMods(new TestModSelectOverlay()) { @@ -98,9 +98,9 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestUnrankedBadge() { AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() })); - AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); + AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 1); AddStep(@"Clear selected mod", () => changeMods(Array.Empty())); - AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); + AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType().Single().Alpha == 0); } private void changeMods(IReadOnlyList mods) => footerButtonMods.Current.Value = mods; @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.UserInterface } } - private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods + private partial class TestScreenFooterButtonMods : FooterButtonMods { public new OsuSpriteText MultiplierText => base.MultiplierText; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs index 08c0c92285..b59a31c173 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs @@ -20,7 +20,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var scoreInfo in getTestScores()) { - fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, IsPersonalBest = scoreInfo.User.Id == 2, @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 foreach (var scoreInfo in getTestScores()) { - fillFlow.Add(new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, IsPersonalBest = scoreInfo.User.Id == 2, @@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Test] public void TestUseTheseModsDoesNotCopySystemMods() { - LeaderboardScoreV2 score = null!; + BeatmapLeaderboardScore score = null!; AddStep("create content", () => { @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Date = DateTimeOffset.Now.AddYears(-2), }; - fillFlow.Add(score = new LeaderboardScoreV2(scoreInfo) + fillFlow.Add(score = new BeatmapLeaderboardScore(scoreInfo) { Rank = scoreInfo.Position, Shear = Vector2.Zero, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs index 1947721d5d..53a1355fc2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -18,14 +19,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene + public partial class TestScenePanelBeatmap : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselV2DifficultyPanel() + public TestScenePanelBeatmap() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs index 2dbe9e6cd1..4adee17868 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -18,14 +19,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene + public partial class TestScenePanelBeatmapStandalone : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapInfo beatmap = null!; - public TestSceneBeatmapCarouselV2StandalonePanel() + public TestScenePanelBeatmapStandalone() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs similarity index 96% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs index d62aee77f3..54c6cb1c0e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -6,15 +6,16 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Visual.UserInterface; using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene + public partial class TestScenePanelGroup : ThemeComparisonTestScene { - public TestSceneBeatmapCarouselV2GroupPanel() + public TestScenePanelGroup() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs similarity index 95% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs index ef34394e12..16f6b2cc9c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Overlays; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; @@ -16,14 +17,14 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene + public partial class TestScenePanelSet : ThemeComparisonTestScene { [Resolved] private BeatmapManager beatmaps { get; set; } = null!; private BeatmapSetInfo beatmapSet = null!; - public TestSceneBeatmapCarouselV2SetPanel() + public TestScenePanelSet() : base(false) { } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs similarity index 90% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs index ba3f2635b0..781691d3db 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelUpdateBeatmapButton.cs @@ -9,14 +9,14 @@ using osu.Game.Screens.SelectV2; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene + public partial class TestScenePanelUpdateBeatmapButton : OsuTestScene { - private UpdateBeatmapSetButton button = null!; + private PanelUpdateBeatmapButton button = null!; [SetUp] public void SetUp() => Schedule(() => { - Child = button = new UpdateBeatmapSetButton + Child = button = new PanelUpdateBeatmapButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs similarity index 98% rename from osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs index 054bbb39d1..bdecebd64f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneScreenFooter.cs @@ -15,9 +15,9 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; -namespace osu.Game.Tests.Visual.UserInterface +namespace osu.Game.Tests.Visual.SongSelectV2 { public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene { @@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.UserInterface screenFooter.SetButtons(new ScreenFooterButton[] { - new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods }, - new ScreenFooterButtonRandom(), - new ScreenFooterButtonOptions(), + new FooterButtonMods(modOverlay) { Current = SelectedMods }, + new FooterButtonRandom(), + new FooterButtonOptions(), }); }); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 630f3c95ee..986ad6fc46 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -22,7 +22,7 @@ using osu.Game.Rulesets.Taiko; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; -using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Screens.SelectV2; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { base.SetUpSteps(); - AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect())); + AddStep("load screen", () => Stack.Push(new SoloSongSelect())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded); } @@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("Press F1", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddAssert("Overlay visible", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); diff --git a/osu.Game/Screens/SelectV2/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs similarity index 99% rename from osu.Game/Screens/SelectV2/Carousel.cs rename to osu.Game/Graphics/Carousel/Carousel.cs index 7b1fd6e999..a9c8aecd6c 100644 --- a/osu.Game/Screens/SelectV2/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -24,7 +24,7 @@ using osu.Game.Input.Bindings; using osuTK; using osuTK.Input; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// A highly efficient vertical list display that is used primarily for the song select screen, diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs similarity index 98% rename from osu.Game/Screens/SelectV2/CarouselItem.cs rename to osu.Game/Graphics/Carousel/CarouselItem.cs index 36dc48a497..223c8d9869 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -3,7 +3,7 @@ using System; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// Represents a single display item for display in a . diff --git a/osu.Game/Screens/SelectV2/ICarouselFilter.cs b/osu.Game/Graphics/Carousel/ICarouselFilter.cs similarity index 95% rename from osu.Game/Screens/SelectV2/ICarouselFilter.cs rename to osu.Game/Graphics/Carousel/ICarouselFilter.cs index f510a7cd4b..570f480aab 100644 --- a/osu.Game/Screens/SelectV2/ICarouselFilter.cs +++ b/osu.Game/Graphics/Carousel/ICarouselFilter.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// An interface representing a filter operation which can be run on a . diff --git a/osu.Game/Screens/SelectV2/ICarouselPanel.cs b/osu.Game/Graphics/Carousel/ICarouselPanel.cs similarity index 97% rename from osu.Game/Screens/SelectV2/ICarouselPanel.cs rename to osu.Game/Graphics/Carousel/ICarouselPanel.cs index 4fba0d2827..5f0ebc263c 100644 --- a/osu.Game/Screens/SelectV2/ICarouselPanel.cs +++ b/osu.Game/Graphics/Carousel/ICarouselPanel.cs @@ -5,7 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; -namespace osu.Game.Screens.SelectV2 +namespace osu.Game.Graphics.Carousel { /// /// An interface to be attached to any s which are used for display inside a . diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs index 4736ba28db..401053599e 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengeLeaderboard.cs @@ -17,7 +17,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens.SelectV2.Leaderboards; +using osu.Game.Screens.SelectV2; using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge @@ -40,7 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private readonly Room room; private readonly PlaylistItem playlistItem; - private FillFlowContainer scoreFlow = null!; + private FillFlowContainer scoreFlow = null!; private Container userBestContainer = null!; private SectionHeader userBestHeader = null!; private LoadingLayer loadingLayer = null!; @@ -91,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - Child = scoreFlow = new FillFlowContainer + Child = scoreFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge } else { - LoadComponentsAsync(best.Select((s, index) => new LeaderboardScoreV2(s, sheared: false) + LoadComponentsAsync(best.Select((s, index) => new BeatmapLeaderboardScore(s, sheared: false) { Rank = index + 1, IsPersonalBest = s.UserID == api.LocalUser.Value.Id, @@ -178,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge if (userBest != null) { - userBestContainer.Add(new LeaderboardScoreV2(userBest, sheared: false) + userBestContainer.Add(new BeatmapLeaderboardScore(userBest, sheared: false) { Rank = userBest.Position, IsPersonalBest = true, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 994b0fb6c0..9cb7d152de 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Pooling; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Select; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 8f9d5cc31b..3360437544 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs index 3cdbbb4fed..22a67321db 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterSorting.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; using osu.Game.Utils; diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs similarity index 99% rename from osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs rename to osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index 0b7b2ebbc1..c9413a9414 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -41,9 +41,9 @@ using osuTK; using osuTK.Graphics; using CommonStrings = osu.Game.Localisation.CommonStrings; -namespace osu.Game.Screens.SelectV2.Leaderboards +namespace osu.Game.Screens.SelectV2 { - public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip + public partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { public Bindable> SelectedMods = new Bindable>(); @@ -117,7 +117,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); public virtual ScoreInfo TooltipContent => score; - public LeaderboardScoreV2(ScoreInfo score, bool sheared = true) + public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) { this.score = score; this.sheared = sheared; diff --git a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs b/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs deleted file mode 100644 index fb2e32dfdc..0000000000 --- a/osu.Game/Screens/SelectV2/Footer/BeatmapOptionsPopover.cs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Collections; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Localisation; -using osu.Game.Overlays; -using osuTK; -using osuTK.Graphics; -using osuTK.Input; -using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; - -namespace osu.Game.Screens.SelectV2.Footer -{ - public partial class BeatmapOptionsPopover : OsuPopover - { - private FillFlowContainer buttonFlow = null!; - private readonly ScreenFooterButtonOptions footerButton; - - [Cached] - private readonly OverlayColourProvider colourProvider; - - private WorkingBeatmap beatmapWhenOpening = null!; - - [Resolved] - private IBindable beatmap { get; set; } = null!; - - public BeatmapOptionsPopover(ScreenFooterButtonOptions footerButton, OverlayColourProvider colourProvider) - { - this.footerButton = footerButton; - this.colourProvider = colourProvider; - } - - [BackgroundDependencyLoader] - private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager) - { - Content.Padding = new MarginPadding(5); - - Child = buttonFlow = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(3), - }; - - beatmapWhenOpening = beatmap.Value; - - addHeader(CommonStrings.General); - addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); - - addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); - addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo); - - addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); - // TODO: make work, and make show "unplayed" or "played" based on status. - addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); - addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo); - - // if (songSelect != null && songSelect.AllowEditing) - addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo); - - addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this)); - - beatmap.BindValueChanged(_ => Hide()); - } - - private void addHeader(LocalisableString text, string? context = null) - { - var textFlow = new OsuTextFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Padding = new MarginPadding(10), - }; - - textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); - - if (context != null) - { - textFlow.NewLine(); - textFlow.AddText(context, t => - { - t.Colour = colourProvider.Content2; - t.Font = t.Font.With(size: 13); - }); - } - - buttonFlow.Add(textFlow); - } - - private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null) - { - var button = new OptionButton - { - Text = text, - Icon = icon, - TextColour = colour, - Action = () => - { - Scheduler.AddDelayed(Hide, 50); - action?.Invoke(); - }, - }; - - buttonFlow.Add(button); - } - - private partial class OptionButton : OsuButton - { - public IconUsage Icon { get; init; } - public Color4? TextColour { get; init; } - - public OptionButton() - { - Size = new Vector2(265, 50); - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - BackgroundColour = colourProvider.Background3; - - SpriteText.Colour = TextColour ?? Color4.White; - Content.CornerRadius = 10; - - Add(new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(17), - X = 15, - Icon = Icon, - Colour = TextColour ?? Color4.White, - }); - } - - protected override SpriteText CreateText() => new OsuSpriteText - { - Depth = -1, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - X = 40 - }; - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - // don't absorb control as ToolbarRulesetSelector uses control + number to navigate - if (e.ControlPressed) return false; - - if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) - { - int requested = e.Key - Key.Number1; - - OptionButton? found = buttonFlow.Children.OfType().ElementAtOrDefault(requested); - - if (found != null) - { - found.TriggerClick(); - return true; - } - } - - return base.OnKeyDown(e); - } - - protected override void UpdateState(ValueChangedEvent state) - { - base.UpdateState(state); - footerButton.OverlayState.Value = state.NewValue; - } - } -} diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/FooterButtonMods.cs similarity index 98% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs rename to osu.Game/Screens/SelectV2/FooterButtonMods.cs index 61d69ae197..833ea96139 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonMods.cs @@ -28,9 +28,9 @@ using osu.Game.Utils; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonMods : ScreenFooterButton, IHasCurrentValue> + public partial class FooterButtonMods : ScreenFooterButton, IHasCurrentValue> { private const float bar_height = 30f; private const float mod_display_portion = 0.65f; @@ -58,7 +58,7 @@ namespace osu.Game.Screens.SelectV2.Footer [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public ScreenFooterButtonMods(ModSelectOverlay overlay) + public FooterButtonMods(ModSelectOverlay overlay) : base(overlay) { } diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs similarity index 76% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs rename to osu.Game/Screens/SelectV2/FooterButtonOptions.cs index 72409b5566..41edaf2a02 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonOptions.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions.cs @@ -5,15 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Screens.Footer; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonOptions : ScreenFooterButton, IHasPopover + public partial class FooterButtonOptions : ScreenFooterButton, IHasPopover { [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -29,6 +28,6 @@ namespace osu.Game.Screens.SelectV2.Footer Action = this.ShowPopover; } - public Popover GetPopover() => new BeatmapOptionsPopover(this, colourProvider); + public Framework.Graphics.UserInterface.Popover GetPopover() => new Popover(this, colourProvider); } } diff --git a/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs new file mode 100644 index 0000000000..76b841ee99 --- /dev/null +++ b/osu.Game/Screens/SelectV2/FooterButtonOptions_Popover.cs @@ -0,0 +1,198 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; +using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class FooterButtonOptions + { + public partial class Popover : OsuPopover + { + private FillFlowContainer buttonFlow = null!; + private readonly FooterButtonOptions footerButton; + + [Cached] + private readonly OverlayColourProvider colourProvider; + + private WorkingBeatmap beatmapWhenOpening = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public Popover(FooterButtonOptions footerButton, OverlayColourProvider colourProvider) + { + this.footerButton = footerButton; + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load(ManageCollectionsDialog? manageCollectionsDialog, OsuColour colours, BeatmapManager? beatmapManager) + { + Content.Padding = new MarginPadding(5); + + Child = buttonFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(3), + }; + + beatmapWhenOpening = beatmap.Value; + + addHeader(CommonStrings.General); + addButton(SongSelectStrings.ManageCollections, FontAwesome.Solid.Book, () => manageCollectionsDialog?.Show()); + + addHeader(SongSelectStrings.ForAllDifficulties, beatmapWhenOpening.BeatmapSetInfo.ToString()); + addButton(SongSelectStrings.DeleteBeatmap, FontAwesome.Solid.Trash, () => { }, colours.Red1); // songSelect?.DeleteBeatmap(beatmapWhenOpening.BeatmapSetInfo); + + addHeader(SongSelectStrings.ForSelectedDifficulty, beatmapWhenOpening.BeatmapInfo.DifficultyName); + // TODO: make work, and make show "unplayed" or "played" based on status. + addButton(SongSelectStrings.MarkAsPlayed, FontAwesome.Regular.TimesCircle, null); + addButton(SongSelectStrings.ClearAllLocalScores, FontAwesome.Solid.Eraser, () => { }, colours.Red1); // songSelect?.ClearScores(beatmapWhenOpening.BeatmapInfo); + + // if (songSelect != null && songSelect.AllowEditing) + addButton(SongSelectStrings.EditBeatmap, FontAwesome.Solid.PencilAlt, () => { }); // songSelect.Edit(beatmapWhenOpening.BeatmapInfo); + + addButton(WebCommonStrings.ButtonsHide.ToSentence(), FontAwesome.Solid.Magic, () => beatmapManager?.Hide(beatmapWhenOpening.BeatmapInfo)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingFocusManager()!.ChangeFocus(this)); + + beatmap.BindValueChanged(_ => Hide()); + } + + private void addHeader(LocalisableString text, string? context = null) + { + var textFlow = new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding(10), + }; + + textFlow.AddText(text, t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); + + if (context != null) + { + textFlow.NewLine(); + textFlow.AddText(context, t => + { + t.Colour = colourProvider.Content2; + t.Font = t.Font.With(size: 13); + }); + } + + buttonFlow.Add(textFlow); + } + + private void addButton(LocalisableString text, IconUsage icon, Action? action, Color4? colour = null) + { + var button = new OptionButton + { + Text = text, + Icon = icon, + TextColour = colour, + Action = () => + { + Scheduler.AddDelayed(Hide, 50); + action?.Invoke(); + }, + }; + + buttonFlow.Add(button); + } + + private partial class OptionButton : OsuButton + { + public IconUsage Icon { get; init; } + public Color4? TextColour { get; init; } + + public OptionButton() + { + Size = new Vector2(265, 50); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + BackgroundColour = colourProvider.Background3; + + SpriteText.Colour = TextColour ?? Color4.White; + Content.CornerRadius = 10; + + Add(new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(17), + X = 15, + Icon = Icon, + Colour = TextColour ?? Color4.White, + }); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40 + }; + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + // don't absorb control as ToolbarRulesetSelector uses control + number to navigate + if (e.ControlPressed) return false; + + if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) + { + int requested = e.Key - Key.Number1; + + OptionButton? found = buttonFlow.Children.OfType().ElementAtOrDefault(requested); + + if (found != null) + { + found.TriggerClick(); + return true; + } + } + + return base.OnKeyDown(e); + } + + protected override void UpdateState(ValueChangedEvent state) + { + base.UpdateState(state); + footerButton.OverlayState.Value = state.NewValue; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs b/osu.Game/Screens/SelectV2/FooterButtonRandom.cs similarity index 97% rename from osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs rename to osu.Game/Screens/SelectV2/FooterButtonRandom.cs index dbdb6fe79b..88b139da97 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonRandom.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonRandom.cs @@ -14,9 +14,9 @@ using osu.Game.Screens.Footer; using osuTK; using osuTK.Input; -namespace osu.Game.Screens.SelectV2.Footer +namespace osu.Game.Screens.SelectV2 { - public partial class ScreenFooterButtonRandom : ScreenFooterButton + public partial class FooterButtonRandom : ScreenFooterButton { public Action? NextRandom { get; set; } public Action? PreviousRandom { get; set; } diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/Panel.cs similarity index 98% rename from osu.Game/Screens/SelectV2/PanelBase.cs rename to osu.Game/Screens/SelectV2/Panel.cs index 32da02a189..c22a88a55f 100644 --- a/osu.Game/Screens/SelectV2/PanelBase.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -19,7 +20,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public abstract partial class PanelBase : PoolableDrawable, ICarouselPanel + public abstract partial class Panel : PoolableDrawable, ICarouselPanel { private const float corner_radius = 10; diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index d4bf3519fa..6742577389 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -22,7 +23,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmap : PanelBase + public partial class PanelBeatmap : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.SelectV2 private ConstrainedIconContainer difficultyIcon = null!; private OsuSpriteText keyCountText = null!; private StarRatingDisplay starRatingDisplay = null!; - private TopLocalRank difficultyRank = null!; + private PanelLocalRankDisplay localRank = null!; private OsuSpriteText difficultyText = null!; private OsuSpriteText authorText = null!; @@ -100,7 +101,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - difficultyRank = new TopLocalRank + localRank = new PanelLocalRankDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -174,7 +175,7 @@ namespace osu.Game.Screens.SelectV2 difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); - difficultyRank.Beatmap = beatmap; + localRank.Beatmap = beatmap; difficultyText.Text = beatmap.DifficultyName; authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); @@ -186,7 +187,7 @@ namespace osu.Game.Screens.SelectV2 { base.FreeAfterUse(); - difficultyRank.Beatmap = null; + localRank.Beatmap = null; starDifficultyBindable = null; } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 9e9ef612ea..179d4d6444 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -11,22 +11,23 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapSet : PanelBase + public partial class PanelBeatmapSet : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; - private BeatmapSetPanelBackground background = null!; + private PanelSetBackground background = null!; private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; private Drawable chevronIcon = null!; - private UpdateBeatmapSetButton updateButton = null!; + private PanelUpdateBeatmapButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private DifficultySpectrumDisplay difficultiesDisplay = null!; @@ -60,7 +61,7 @@ namespace osu.Game.Screens.SelectV2 }, }; - Background = background = new BeatmapSetPanelBackground + Background = background = new PanelSetBackground { RelativeSizeAxes = Axes.Both, }; @@ -89,7 +90,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButton + updateButton = new PanelUpdateBeatmapButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index f893bb0caf..a0d7484587 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -13,6 +13,7 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; @@ -23,7 +24,7 @@ using osuTK; namespace osu.Game.Screens.SelectV2 { - public partial class PanelBeatmapStandalone : PanelBase + public partial class PanelBeatmapStandalone : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; @@ -48,17 +49,17 @@ namespace osu.Game.Screens.SelectV2 private IBindable? starDifficultyBindable; private CancellationTokenSource? starDifficultyCancellationSource; - private BeatmapSetPanelBackground background = null!; + private PanelSetBackground background = null!; private OsuSpriteText titleText = null!; private OsuSpriteText artistText = null!; - private UpdateBeatmapSetButton updateButton = null!; + private PanelUpdateBeatmapButton updateButton = null!; private BeatmapSetOnlineStatusPill statusPill = null!; private ConstrainedIconContainer difficultyIcon = null!; private FillFlowContainer difficultyLine = null!; private StarRatingDisplay difficultyStarRating = null!; - private TopLocalRank difficultyRank = null!; + private PanelLocalRankDisplay difficultyRank = null!; private OsuSpriteText difficultyKeyCountText = null!; private OsuSpriteText difficultyName = null!; private OsuSpriteText difficultyAuthor = null!; @@ -80,7 +81,7 @@ namespace osu.Game.Screens.SelectV2 Colour = colourProvider.Background5, }; - Background = background = new BeatmapSetPanelBackground + Background = background = new PanelSetBackground { RelativeSizeAxes = Axes.Both, }; @@ -109,7 +110,7 @@ namespace osu.Game.Screens.SelectV2 Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { - updateButton = new UpdateBeatmapSetButton + updateButton = new PanelUpdateBeatmapButton { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -136,7 +137,7 @@ namespace osu.Game.Screens.SelectV2 Scale = new Vector2(8f / 9f), Margin = new MarginPadding { Right = 5f }, }, - difficultyRank = new TopLocalRank + difficultyRank = new PanelLocalRankDisplay { Scale = new Vector2(8f / 11), Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index a5786b53c9..ac4857d2f3 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; @@ -18,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelGroup : PanelBase + public partial class PanelGroup : Panel { public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.2f; diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index ce46362133..4ef3bd724c 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class PanelGroupStarDifficulty : PanelBase + public partial class PanelGroupStarDifficulty : Panel { [Resolved] private OsuColour colours { get; set; } = null!; diff --git a/osu.Game/Screens/SelectV2/TopLocalRank.cs b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs similarity index 96% rename from osu.Game/Screens/SelectV2/TopLocalRank.cs rename to osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs index 2a72a05db7..588e7e650e 100644 --- a/osu.Game/Screens/SelectV2/TopLocalRank.cs +++ b/osu.Game/Screens/SelectV2/PanelLocalRankDisplay.cs @@ -19,7 +19,7 @@ using Realms; namespace osu.Game.Screens.SelectV2 { - public partial class TopLocalRank : CompositeDrawable + public partial class PanelLocalRankDisplay : CompositeDrawable { private BeatmapInfo? beatmap; @@ -48,7 +48,7 @@ namespace osu.Game.Screens.SelectV2 private readonly UpdateableRank updateable; - public TopLocalRank(BeatmapInfo? beatmap = null) + public PanelLocalRankDisplay(BeatmapInfo? beatmap = null) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/PanelSetBackground.cs similarity index 97% rename from osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs rename to osu.Game/Screens/SelectV2/PanelSetBackground.cs index 798acf62ee..99dbf90556 100644 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs +++ b/osu.Game/Screens/SelectV2/PanelSetBackground.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapSetPanelBackground : ModelBackedDrawable + public partial class PanelSetBackground : ModelBackedDrawable { protected override double TransformDuration => 400; diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs similarity index 98% rename from osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs rename to osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs index ac7e3856ac..2a850321a6 100644 --- a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs +++ b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.SelectV2 { - public partial class UpdateBeatmapSetButton : OsuAnimatedButton + public partial class PanelUpdateBeatmapButton : OsuAnimatedButton { private BeatmapSetInfo? beatmapSet; @@ -53,7 +53,7 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IDialogOverlay? dialogOverlay { get; set; } - public UpdateBeatmapSetButton() + public PanelUpdateBeatmapButton() { Size = new Vector2(75f, 22f); } diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index e295656a21..67ca110dab 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -11,7 +11,6 @@ using osu.Game.Overlays.Mods; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; -using osu.Game.Screens.SelectV2.Footer; namespace osu.Game.Screens.SelectV2 { @@ -77,9 +76,9 @@ namespace osu.Game.Screens.SelectV2 public override IReadOnlyList CreateFooterButtons() => new ScreenFooterButton[] { - new ScreenFooterButtonMods(modSelectOverlay) { Current = Mods }, - new ScreenFooterButtonRandom(), - new ScreenFooterButtonOptions(), + new FooterButtonMods(modSelectOverlay) { Current = Mods }, + new FooterButtonRandom(), + new FooterButtonOptions(), }; protected override void LoadComplete() From 6fe1695d39d4c9f774807ad3bc8623c45587d099 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 19:40:47 +0900 Subject: [PATCH 228/281] Use full namespace isntead of weird using statement --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 0afeaa9532..85a87b0dff 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -38,7 +38,6 @@ using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; using osu.Game.Localisation; -using WebLocalisation = osu.Game.Resources.Localisation.Web; namespace osu.Game.Screens.OnlinePlay { @@ -561,7 +560,7 @@ namespace osu.Game.Screens.OnlinePlay Size = new Vector2(30, 30), Alpha = AllowEditing ? 1 : 0, Action = () => RequestEdit?.Invoke(Item), - TooltipText = WebLocalisation.CommonStrings.ButtonsEdit + TooltipText = Resources.Localisation.Web.CommonStrings.ButtonsEdit }, removeButton = new PlaylistRemoveButton { From 6021d85e633915a0092923ecf16f06fb1554ce66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:41:05 +0900 Subject: [PATCH 229/281] Add keywords for converted setting --- .../Settings/Sections/UserInterface/SongSelectSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index cb0d738a2c..d15008f858 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -23,6 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { LabelText = UserInterfaceStrings.ShowConvertedBeatmaps, Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + Keywords = new[] { "converts", "converted" } }, new SettingsEnumDropdown { From 2ac1b8903727a49ababc1c89952895d1c98e7f1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:42:32 +0900 Subject: [PATCH 230/281] Make some test methods static for future reuse --- .../SongSelect/TestSceneBeatmapInfoWedge.cs | 24 +++++++++---------- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 12 ++++++---- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index d8573b2d03..8132f8a841 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.SongSelect foreach (var rulesetInfo in rulesets.AvailableRulesets) { var instance = rulesetInfo.CreateInstance(); - var testBeatmap = createTestBeatmap(rulesetInfo); + var testBeatmap = CreateTestBeatmap(rulesetInfo); beatmaps.Add(testBeatmap); @@ -124,6 +124,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("reset mods", () => SelectedMods.SetDefault()); } + [Test] + public void TestTruncation() + { + selectBeatmap(CreateLongMetadata()); + } + [Test] public void TestNullBeatmap() { @@ -135,17 +141,11 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); } - [Test] - public void TestTruncation() - { - selectBeatmap(createLongMetadata()); - } - [Test] public void TestBPMUpdates() { const double bpm = 120; - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm }); OsuModDoubleTime doubleTime = null!; @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase(120, 120.4, "DT", "180")] public void TestVaryingBPM(double commonBpm, double otherBpm, string? mod, string expectedDisplay) { - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm }); beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm }); @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase] public void TestLengthUpdates() { - IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo); + IBeatmap beatmap = CreateTestBeatmap(new OsuRuleset().RulesetInfo); double drain = beatmap.CalculateDrainLength(); beatmap.BeatmapInfo.Length = drain; @@ -248,7 +248,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore); } - private IBeatmap createTestBeatmap(RulesetInfo ruleset) + public static IBeatmap CreateTestBeatmap(RulesetInfo ruleset) { List objects = new List(); for (double i = 0; i < 50000; i += 1000) @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Visual.SongSelect }; } - private IBeatmap createLongMetadata() + public static IBeatmap CreateLongMetadata() { return new Beatmap { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 45381b3e02..70f2fb1361 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect private RulesetStore rulesetStore = null!; private BeatmapManager beatmapManager = null!; private PlaySongSelect songSelect = null!; + private LeaderboardManager leaderboardManager = null!; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -52,9 +53,10 @@ namespace osu.Game.Tests.Visual.SongSelect dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); dependencies.CacheAs(songSelect = new PlaySongSelect()); - Dependencies.Cache(Realm); dependencies.Cache(leaderboardManager = new LeaderboardManager()); + Dependencies.Cache(Realm); + return dependencies; } @@ -204,8 +206,8 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestGlobalScoresDisplay() { AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global); - AddStep(@"New Scores", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()))); - AddStep(@"New Scores with teams", () => leaderboard.SetScores(generateSampleScores(new BeatmapInfo()).Select(s => + AddStep(@"New Scores", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(GenerateSampleScores(new BeatmapInfo()).Select(s => { s.User.Team = new APITeam(); return s; @@ -310,7 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep(@"Import new scores", () => { - foreach (var score in generateSampleScores(beatmapInfo())) + foreach (var score in GenerateSampleScores(beatmapInfo())) scoreManager.Import(score); }); } @@ -326,7 +328,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void checkStoredCount(int expected) => AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All().Count(s => !s.DeletePending)), () => Is.EqualTo(expected)); - private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo) + public static ScoreInfo[] GenerateSampleScores(BeatmapInfo beatmapInfo) { return new[] { From 4c1f4a512cb89f36b4a30343267cc8cdb03c38a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:42:57 +0900 Subject: [PATCH 231/281] Avoid adding arbitrary background in `SongSelectComponentsTestScene` --- .../SongSelectV2/SongSelectComponentsTestScene.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 8694722acc..9e9cd3505a 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Overlays; @@ -20,7 +19,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), }; private Container? resizeContainer; @@ -33,15 +31,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), Width = relativeWidth, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5, - }, Content } }; @@ -55,6 +47,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + protected override void LoadComplete() + { + base.LoadComplete(); + ChangeBackgroundColour(ColourProvider.Background6); + } + [SetUpSteps] public virtual void SetUpSteps() { From 1cca936e285b008d136d279a4ea8773416a4f4a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:43:32 +0900 Subject: [PATCH 232/281] Add global screen margin for new screen designs --- osu.Game/OsuGame.cs | 5 +++++ osu.Game/Screens/Footer/ScreenFooter.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 70a324cd8e..0c6a06a8fc 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -104,6 +104,11 @@ namespace osu.Game /// public static readonly Vector2 SHEAR = new Vector2(0.2f, 0); + /// + /// For elements placed close to the screen edge, this is the margin to leave to the edge. + /// + public const float SCREEN_EDGE_MARGIN = 12f; + public Toolbar Toolbar { get; private set; } private ChatOverlay chatOverlay; diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index f75250a832..94f4ceeb1a 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -104,14 +104,14 @@ namespace osu.Game.Screens.Footer }, BackButton = new ScreenBackButton { - Margin = new MarginPadding { Bottom = 15f, Left = 12f }, + Margin = new MarginPadding { Bottom = OsuGame.SCREEN_EDGE_MARGIN, Left = OsuGame.SCREEN_EDGE_MARGIN }, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Action = onBackPressed, }, hiddenButtonsContainer = new Container { - Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, + Margin = new MarginPadding { Left = OsuGame.SCREEN_EDGE_MARGIN + ScreenBackButton.BUTTON_WIDTH + padding }, Y = 10f, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, From 376b4e89299f61b05f441ebdee1923a245f4aa66 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:45:48 +0900 Subject: [PATCH 233/281] Disable masking of `Carousel` The default for carousels should be unmasked as their usage generally sees them overflowing outside their main usage area (see `bleed` variables). --- osu.Game/Graphics/Carousel/Carousel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index a9c8aecd6c..3a02eb7119 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -228,6 +228,7 @@ namespace osu.Game.Graphics.Carousel { InternalChild = Scroll = new CarouselScrollContainer { + Masking = false, RelativeSizeAxes = Axes.Both, }; From 07d0c7443c6f9749277377b882fbf3211d0d282e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 18:46:37 +0900 Subject: [PATCH 234/281] Add animated fade when online status pill has an unknown status --- osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 7b3067e8d6..c6a3c7db3c 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -100,7 +100,7 @@ namespace osu.Game.Beatmaps.Drawables { if (Status == BeatmapOnlineStatus.None && !ShowUnknownStatus) { - Hide(); + this.FadeOut(animation_duration, Easing.OutQuint); return; } From 51ad6289ca4b2417c02ae928957f8726551982da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Apr 2025 20:21:48 +0900 Subject: [PATCH 235/281] Fix global offset adjust control showing adjustment available when it shouldn't MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audio offset is integer based in configuration, so let's make sure not to show that there's an applicable offset when the value difference is too low. I've also fixed rounding to match expectations (`AudioOffset` is precision limited to integer), and handled the case where a user adjusts the slider but also has a suggested offset – previously it would not enable the button after slider adjustments but now it will work as expected. --- ...estSceneHitEventTimingDistributionGraph.cs | 13 +++++ .../TestSceneAudioOffsetAdjustControl.cs | 51 +++++++++++++++++++ osu.Game/Localisation/AudioSettingsStrings.cs | 5 ++ .../Audio/AudioOffsetAdjustControl.cs | 22 ++++++-- 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 760210c370..bb4b785db0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -168,6 +168,19 @@ namespace osu.Game.Tests.Visual.Ranking }; }); + public static List CreateHitEvents(double offset = 0, int count = 50) + { + var hitEvents = new List(); + + for (int i = 0; i < count; i++) + { + for (int j = 0; j < count; j++) + hitEvents.Add(new HitEvent(offset, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)); + } + + return hitEvents; + } + public static List CreateDistributedHitEvents(double centre = 0, double range = 25) { var hitEvents = new List(); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs index 85cde966b1..2fc5378ba1 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneAudioOffsetAdjustControl.cs @@ -1,11 +1,14 @@ // 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.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Overlays.Settings.Sections.Audio; @@ -70,16 +73,54 @@ namespace osu.Game.Tests.Visual.Settings AddStep("clear history", () => tracker.ClearHistory()); } + [Test] + public void TestRounding() + { + AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateHitEvents(0.6), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + })); + + checkButtonEnabled(); + AddStep("click button", () => adjustControl.ChildrenOfType public static LocalisableString SuggestedOffsetNote => new TranslatableString(getKey(@"suggested_offset_note"), @"Play a few beatmaps to receive a suggested offset!"); + /// + /// "Based on the last {0} play(s), your offset is set correctly!" + /// + public static LocalisableString SuggestedOffsetCorrect(int plays) => new TranslatableString(getKey(@"suggested_offset_correct"), @"Based on the last {0} play(s), your offset is set correctly!", plays); + /// /// "Based on the last {0} play(s), the suggested offset is {1} ms." /// diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index b9f043a233..04496428ee 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -109,6 +109,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio base.LoadComplete(); averageHitErrorHistory.BindCollectionChanged(updateDisplay, true); + current.BindValueChanged(_ => updateHintText()); SuggestedOffset.BindValueChanged(_ => updateHintText(), true); } @@ -148,17 +149,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio break; } - SuggestedOffset.Value = averageHitErrorHistory.Any() ? averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null; + SuggestedOffset.Value = averageHitErrorHistory.Any() ? Math.Round(averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset)) : null; } private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue)); private void updateHintText() { - hintText.Text = SuggestedOffset.Value == null - ? AudioSettingsStrings.SuggestedOffsetNote - : AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0")); - applySuggestion.Enabled.Value = SuggestedOffset.Value != null; + if (SuggestedOffset.Value == null) + { + applySuggestion.Enabled.Value = false; + hintText.Text = AudioSettingsStrings.SuggestedOffsetNote; + } + else if (Math.Abs(SuggestedOffset.Value.Value - current.Value) < 1) + { + applySuggestion.Enabled.Value = false; + hintText.Text = AudioSettingsStrings.SuggestedOffsetCorrect(averageHitErrorHistory.Count); + } + else + { + applySuggestion.Enabled.Value = true; + hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0")); + } } private partial class OffsetSliderBar : RoundedSliderBar From c231571f06167b4445148bf29ac70c4facb3f8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 16 Apr 2025 13:46:35 +0200 Subject: [PATCH 236/281] Separate gameplay leaderboard data management from display This is a prerequisite for supporting skinning of leaderboards. - New `IGameplayLeaderboardProvider` and `IGameplayLeaderboardScore` interfaces are introduced. They are strictly concerned with supplying leaderboard data. - Logic of managing display, which was previously jammed into the inheritance hierarchy of `GameplayLeaderboard`, is now moved into `IGameplayLeaderboardProvider` implementations. Solo play, multiplayer, and multiplayer spectator get their own implementation of the interface. - The inheritance hierarchy of `GameplayLeaderboard` and per-player overriding of the implementation of the gameplay leaderboard is gone. Only one drawable class (renamed to `DrawableGameplayLeaderboard`) is allowed to display the leaderboards, across all modes of play. --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 81 ++++++++++-- .../TestSceneSoloGameplayLeaderboard.cs | 124 ------------------ ...MultiplayerGameplayLeaderboardTestScene.cs | 39 +++++- .../TestSceneMultiSpectatorLeaderboard.cs | 31 +++-- .../TestSceneMultiSpectatorScreen.cs | 2 +- ...TestSceneMultiplayerGameplayLeaderboard.cs | 16 +-- ...ceneMultiplayerGameplayLeaderboardTeams.cs | 15 ++- .../Online/Leaderboards/LeaderboardManager.cs | 6 +- .../Multiplayer/MultiplayerPlayer.cs | 51 +++---- .../Spectate/MultiSpectatorScreen.cs | 28 ++-- ...oard.cs => DrawableGameplayLeaderboard.cs} | 59 +++++---- ...cs => DrawableGameplayLeaderboardScore.cs} | 26 ++-- .../Play/HUD/IGameplayLeaderboardScore.cs | 67 ++++++++++ .../Screens/Play/HUD/ILeaderboardScore.cs | 31 ----- .../Play/HUD/SoloGameplayLeaderboard.cs | 108 --------------- osu.Game/Screens/Play/Player.cs | 28 ++-- osu.Game/Screens/Play/ReplayPlayer.cs | 38 ++---- osu.Game/Screens/Play/SoloPlayer.cs | 57 ++------ .../Leaderboards/GameplayLeaderboardScore.cs | 59 +++++++++ .../IGameplayLeaderboardProvider.cs | 25 ++++ .../MultiSpectatorLeaderboardProvider.cs} | 7 +- .../MultiplayerLeaderboardProvider.cs} | 114 +++++++--------- .../SoloGameplayLeaderboardProvider.cs | 41 ++++++ 23 files changed, 508 insertions(+), 545 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs rename osu.Game/Screens/Play/HUD/{GameplayLeaderboard.cs => DrawableGameplayLeaderboard.cs} (74%) rename osu.Game/Screens/Play/HUD/{GameplayLeaderboardScore.cs => DrawableGameplayLeaderboardScore.cs} (96%) create mode 100644 osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs delete mode 100644 osu.Game/Screens/Play/HUD/ILeaderboardScore.cs delete mode 100644 osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs create mode 100644 osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs create mode 100644 osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs rename osu.Game/Screens/{OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs => Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs} (76%) rename osu.Game/Screens/{Play/HUD/MultiplayerGameplayLeaderboard.cs => Select/Leaderboards/MultiplayerLeaderboardProvider.cs} (68%) create mode 100644 osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 1787230117..23cd262dd0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -1,11 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; @@ -15,7 +15,10 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -23,7 +26,10 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public partial class TestSceneGameplayLeaderboard : OsuTestScene { - private TestGameplayLeaderboard leaderboard; + private TestDrawableGameplayLeaderboard leaderboard = null!; + + [Cached(typeof(IGameplayLeaderboardProvider))] + private TestGameplayLeaderboardProvider leaderboardProvider = new TestGameplayLeaderboardProvider(); private readonly BindableLong playerScore = new BindableLong(); @@ -57,10 +63,10 @@ namespace osu.Game.Tests.Visual.Gameplay // has caused layout to not work in the past. AddUntilStep("wait for fill flow layout", - () => leaderboard.ChildrenOfType>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad)); + () => leaderboard.ChildrenOfType>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad)); AddUntilStep("wait for some scores not masked away", - () => leaderboard.ChildrenOfType().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre))); + () => leaderboard.ChildrenOfType().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre))); AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad)); @@ -139,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay checkHeight(8); void checkHeight(int panelCount) - => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount); + => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount); } [Test] @@ -179,6 +185,27 @@ namespace osu.Game.Tests.Visual.Gameplay () => Does.Contain("#FF549A")); } + [Test] + public void TestTrackedScorePosition([Values] bool partial) + { + createLeaderboard(partial); + + AddStep("add many scores in one go", () => + { + for (int i = 0; i < 49; i++) + createRandomScore(new APIUser { Username = $"Player {i + 1}" }); + + // Add player at end to force an animation down the whole list. + playerScore.Value = 0; + createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true); + }); + + if (partial) + AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); + else + AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); + } + private void addLocalPlayer() { AddStep("add local player", () => @@ -188,11 +215,13 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void createLeaderboard() + private void createLeaderboard(bool partial = false) { AddStep("create leaderboard", () => { - Child = leaderboard = new TestGameplayLeaderboard + leaderboardProvider.Scores.Clear(); + leaderboardProvider.IsPartial = partial; + Child = leaderboard = new TestDrawableGameplayLeaderboard { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -205,11 +234,11 @@ namespace osu.Game.Tests.Visual.Gameplay private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false) { - var leaderboardScore = leaderboard.Add(user, isTracked); - leaderboardScore.TotalScore.BindTo(score); + var leaderboardScore = new TestDrawableGameplayLeaderboardScore(user, isTracked, score); + leaderboardProvider.Scores.Add(leaderboardScore); } - private partial class TestGameplayLeaderboard : GameplayLeaderboard + private partial class TestDrawableGameplayLeaderboard : DrawableGameplayLeaderboard { public float Spacing => Flow.Spacing.Y; @@ -220,8 +249,36 @@ namespace osu.Game.Tests.Visual.Gameplay return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } - public IEnumerable GetAllScoresForUsername(string username) + public IEnumerable GetAllScoresForUsername(string username) => Flow.Where(i => i.User?.Username == username); } + + private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider + { + IBindableList IGameplayLeaderboardProvider.Scores => Scores; + public BindableList Scores { get; } = new BindableList(); + public bool IsPartial { get; set; } + } + + private class TestDrawableGameplayLeaderboardScore : IGameplayLeaderboardScore + { + public IUser User { get; } + public bool Tracked { get; } + public BindableLong TotalScore { get; } = new BindableLong(); + public BindableDouble Accuracy { get; } = new BindableDouble(); + public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); + public Bindable DisplayOrder { get; } = new BindableLong(); + public Func GetDisplayScore { get; set; } + public Colour4? TeamColour => null; + + public TestDrawableGameplayLeaderboardScore(IUser user, bool isTracked, Bindable totalScore) + { + User = user; + Tracked = isTracked; + TotalScore.BindTo(totalScore); + GetDisplayScore = _ => TotalScore.Value; + } + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs deleted file mode 100644 index dbd14db818..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Configuration; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Select; -using osu.Game.Tests.Gameplay; - -namespace osu.Game.Tests.Visual.Gameplay -{ - public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene - { - [Cached(typeof(ScoreProcessor))] - private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor; - - private readonly BindableList scores = new BindableList(); - - private readonly Bindable configVisibility = new Bindable(); - private readonly Bindable beatmapTabType = new Bindable(); - - private SoloGameplayLeaderboard leaderboard = null!; - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); - config.BindWith(OsuSetting.BeatmapDetailTab, beatmapTabType); - } - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("clear scores", () => scores.Clear()); - - AddStep("create component", () => - { - var trackingUser = new APIUser - { - Username = "local user", - Id = 2, - }; - - Child = leaderboard = new SoloGameplayLeaderboard(trackingUser) - { - Scores = { BindTarget = scores }, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AlwaysVisible = { Value = false }, - Expanded = { Value = true }, - }; - }); - - AddStep("add scores", () => scores.AddRange(createSampleScores())); - } - - [Test] - public void TestLocalUser() - { - AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v); - AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v); - AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v); - AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value); - } - - [TestCase(PlayBeatmapDetailArea.TabType.Local, 51)] - [TestCase(PlayBeatmapDetailArea.TabType.Global, null)] - [TestCase(PlayBeatmapDetailArea.TabType.Country, null)] - [TestCase(PlayBeatmapDetailArea.TabType.Friends, null)] - public void TestTrackedScorePosition(PlayBeatmapDetailArea.TabType tabType, int? expectedOverflowIndex) - { - AddStep($"change TabType to {tabType}", () => beatmapTabType.Value = tabType); - AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); - - AddStep("add one more score", () => scores.Add(new ScoreInfo { User = new APIUser { Username = "New player 1" }, TotalScore = RNG.Next(600000, 1000000) })); - - AddUntilStep("wait for sort", () => leaderboard.ChildrenOfType().First().ScorePosition != null); - - if (expectedOverflowIndex == null) - AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); - else - AddUntilStep($"tracked player is #{expectedOverflowIndex}", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(expectedOverflowIndex)); - } - - [Test] - public void TestVisibility() - { - AddStep("set config visible true", () => configVisibility.Value = true); - AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1); - - AddStep("set config visible false", () => configVisibility.Value = false); - AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0); - - AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true); - AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1); - - AddStep("set config visible true", () => configVisibility.Value = true); - AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1); - } - - private static List createSampleScores() - { - return new[] - { - new ScoreInfo { User = new APIUser { Username = @"peppy" }, TotalScore = RNG.Next(500000, 1000000) }, - new ScoreInfo { User = new APIUser { Username = @"smoogipoo" }, TotalScore = RNG.Next(500000, 1000000) }, - new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) }, - new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) }, - new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) }, - }.Concat(Enumerable.Range(0, 44).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList(); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 1eb08ad3c8..644b7f522e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -10,6 +10,7 @@ using Moq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; @@ -20,6 +21,7 @@ using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Tests.Visual.Multiplayer { @@ -29,11 +31,13 @@ namespace osu.Game.Tests.Visual.Multiplayer protected readonly BindableList MultiplayerUsers = new BindableList(); - protected MultiplayerGameplayLeaderboard? Leaderboard { get; private set; } + protected MultiplayerLeaderboardProvider? LeaderboardProvider { get; private set; } + + protected DrawableGameplayLeaderboard? Leaderboard { get; private set; } protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId); - protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(); + protected abstract MultiplayerLeaderboardProvider CreateLeaderboardProvider(); private readonly BindableList multiplayerUserIds = new BindableList(); private readonly BindableDictionary watchedUserStates = new BindableDictionary(); @@ -124,11 +128,21 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create leaderboard", () => { - Leaderboard?.Expire(); + Clear(true); Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - LoadComponentAsync(Leaderboard = CreateLeaderboard(), Add); + LoadComponentAsync(LeaderboardProvider = CreateLeaderboardProvider(), Add); + Add(new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = [(typeof(IGameplayLeaderboardProvider), LeaderboardProvider)], + Child = Leaderboard = new DrawableGameplayLeaderboard + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); }); AddUntilStep("wait for load", () => Leaderboard!.IsLoaded); @@ -159,10 +173,18 @@ namespace osu.Game.Tests.Visual.Multiplayer return false; }); - AddStep("check stop watching requests were sent", () => + AddUntilStep("check stop watching requests were sent", () => { - foreach (var user in MultiplayerUsers) - spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once); + try + { + foreach (var user in MultiplayerUsers) + spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once); + return true; + } + catch + { + return false; + } }); } @@ -204,12 +226,14 @@ namespace osu.Game.Tests.Visual.Multiplayer header.Combo++; header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); header.Statistics[HitResult.Meh]++; + header.TotalScore += 50; break; default: header.Combo++; header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); header.Statistics[HitResult.Great]++; + header.TotalScore += 300; break; } @@ -218,3 +242,4 @@ namespace osu.Game.Tests.Visual.Multiplayer } } } + diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs index 60358dfbc4..806de68f07 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs @@ -9,15 +9,16 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Tests.Visual.Multiplayer { public partial class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene { private Dictionary clocks = null!; - private MultiSpectatorLeaderboard? leaderboard; + private MultiSpectatorLeaderboardProvider? leaderboardProvider; + private DrawableGameplayLeaderboard leaderboard = null!; [SetUpSteps] public override void SetUpSteps() @@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("reset", () => { - leaderboard?.RemoveAndDisposeImmediately(); + Clear(true); clocks = new Dictionary { @@ -48,21 +49,27 @@ namespace osu.Game.Tests.Visual.Multiplayer { Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) + LoadComponentAsync(leaderboardProvider = new MultiSpectatorLeaderboardProvider(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()), Add); + Add(new DependencyProvidingContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Expanded = { Value = true } - }, Add); + RelativeSizeAxes = Axes.Both, + CachedDependencies = [(typeof(IGameplayLeaderboardProvider), leaderboardProvider)], + Child = leaderboard = new DrawableGameplayLeaderboard + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Expanded = { Value = true } + } + }); }); - AddUntilStep("wait for load", () => leaderboard!.IsLoaded); - AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType().Count() == 2); + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + AddUntilStep("wait for user population", () => leaderboard.ChildrenOfType().Count() == 2); AddStep("add clock sources", () => { foreach ((int userId, var clock) in clocks) - leaderboard!.AddClock(userId, clock); + leaderboardProvider!.AddClock(userId, clock); }); } @@ -123,6 +130,6 @@ namespace osu.Game.Tests.Visual.Multiplayer => AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time); private void assertCombo(int userId, int expectedCombo) - => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo); + => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index aa98dc59db..6f6d7b31b5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -560,7 +560,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId); - private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.OnlineID == userId); + private DrawableGameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.OnlineID == userId); private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 2f232a6164..53e265decb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -9,7 +9,7 @@ using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Tests.Visual.Multiplayer { @@ -25,27 +25,25 @@ namespace osu.Game.Tests.Visual.Multiplayer return user; } - protected override MultiplayerGameplayLeaderboard CreateLeaderboard() - { - return new TestLeaderboard(MultiplayerUsers.ToArray()) + protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() => + new TestLeaderboard(MultiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }; - } [Test] public void TestPerUserMods() { - AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)Leaderboard!).UserMods[0], Is.Empty)); + AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[0], Is.Empty)); AddStep("last user has NF mod", () => { - Assert.That(((TestLeaderboard)Leaderboard!).UserMods[TOTAL_USERS - 1], Has.One.Items); - Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf()); + Assert.That(((TestLeaderboard)LeaderboardProvider!).UserMods[TOTAL_USERS - 1], Has.One.Items); + Assert.That(((TestLeaderboard)LeaderboardProvider).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf()); }); } - private partial class TestLeaderboard : MultiplayerGameplayLeaderboard + private partial class TestLeaderboard : MultiplayerLeaderboardProvider { public Dictionary> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs index 3f1db308c0..15efde7abe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboardTeams.cs @@ -7,6 +7,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Tests.Visual.Multiplayer { @@ -24,8 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer return user; } - protected override MultiplayerGameplayLeaderboard CreateLeaderboard() => - new MultiplayerGameplayLeaderboard(MultiplayerUsers.ToArray()) + protected override MultiplayerLeaderboardProvider CreateLeaderboardProvider() => + new MultiplayerLeaderboardProvider(MultiplayerUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -39,17 +40,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { LoadComponentAsync(new MatchScoreDisplay { - Team1Score = { BindTarget = Leaderboard!.TeamScores[0] }, - Team2Score = { BindTarget = Leaderboard.TeamScores[1] } + Team1Score = { BindTarget = LeaderboardProvider!.TeamScores[0] }, + Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] } }, Add); LoadComponentAsync(new GameplayMatchScoreDisplay { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Team1Score = { BindTarget = Leaderboard.TeamScores[0] }, - Team2Score = { BindTarget = Leaderboard.TeamScores[1] }, - Expanded = { BindTarget = Leaderboard.Expanded }, + Team1Score = { BindTarget = LeaderboardProvider.TeamScores[0] }, + Team2Score = { BindTarget = LeaderboardProvider.TeamScores[1] }, + Expanded = { BindTarget = Leaderboard!.Expanded }, }, Add); }); } diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index ff3fe39a96..121f68c12b 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -94,7 +94,7 @@ namespace osu.Game.Online.Leaderboards var result = new LeaderboardScores ( - response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(), + response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore().ToArray(), response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) ); inFlightOnlineRequest = null; @@ -138,7 +138,7 @@ namespace osu.Game.Online.Leaderboards newScores = newScores.Detach().OrderByTotalScore(); - scores.Value = new LeaderboardScores(newScores, null); + scores.Value = new LeaderboardScores(newScores.ToArray(), null); if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource) { @@ -155,7 +155,7 @@ namespace osu.Game.Online.Leaderboards Mod[]? ExactMods ); - public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore) + public record LeaderboardScores(ICollection TopScores, ScoreInfo? UserScore) { public IEnumerable AllScores { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 3d4b46f49e..d6f5529d4a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -15,8 +15,8 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -25,6 +25,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { protected override bool PauseOnFocusLost => false; + protected override bool ShowLeaderboard => true; + protected override UserActivity InitialActivity => new UserActivity.InMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); [Resolved] @@ -33,10 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private IBindable isConnected = null!; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); - private readonly MultiplayerRoomUser[] users; private LoadingLayer loadingDisplay = null!; - private MultiplayerGameplayLeaderboard multiplayerLeaderboard = null!; + + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly MultiplayerLeaderboardProvider leaderboardProvider; + + private GameplayMatchScoreDisplay teamScoreDisplay = null!; /// /// Construct a multiplayer player. @@ -55,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer AlwaysShowLeaderboard = true, }) { - this.users = users; + leaderboardProvider = new MultiplayerLeaderboardProvider(users); } [BackgroundDependencyLoader] @@ -71,26 +76,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Expanded = { BindTarget = LeaderboardExpandedState }, }, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat)); - HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); - } - - protected override GameplayLeaderboard CreateGameplayLeaderboard() => multiplayerLeaderboard = new MultiplayerGameplayLeaderboard(users); - - protected override void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) - { - Debug.Assert(leaderboard == multiplayerLeaderboard); - - HUDOverlay.LeaderboardFlow.Insert(0, leaderboard); - - if (multiplayerLeaderboard.TeamScores.Count >= 2) + LoadComponentAsync(teamScoreDisplay = new GameplayMatchScoreDisplay { - LoadComponentAsync(new GameplayMatchScoreDisplay + Expanded = { BindTarget = HUDOverlay.ShowHud }, + Alpha = 0, + }, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay)); + LoadComponentAsync(leaderboardProvider, loaded => + { + AddInternal(loaded); + + if (loaded.HasTeams) { - Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value }, - Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value }, - Expanded = { BindTarget = HUDOverlay.ShowHud }, - }, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay)); - } + teamScoreDisplay.Alpha = 1; + teamScoreDisplay.Team1Score.BindTarget = leaderboardProvider.TeamScores.First().Value; + teamScoreDisplay.Team2Score.BindTarget = leaderboardProvider.TeamScores.Last().Value; + } + }); + + HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); } protected override void LoadAsyncComplete() @@ -195,8 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { Debug.Assert(Room.RoomID != null); - return multiplayerLeaderboard.TeamScores.Count == 2 - ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores) + return leaderboardProvider.TeamScores.Count == 2 + ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, leaderboardProvider.TeamScores) { IsLocalPlay = true, } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 33c3c60ed3..85b6966eaa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -15,6 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Online.Spectator; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Spectate; using osu.Game.Users; using osuTK; @@ -47,17 +48,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [Resolved] private MultiplayerClient multiplayerClient { get; set; } = null!; + [Cached(typeof(IGameplayLeaderboardProvider))] + private MultiSpectatorLeaderboardProvider leaderboardProvider { get; set; } + private IAggregateAudioAdjustment? boundAdjustments; private readonly PlayerArea[] instances; private MasterGameplayClockContainer masterClockContainer = null!; private SpectatorSyncManager syncManager = null!; private PlayerGrid grid = null!; - private MultiSpectatorLeaderboard leaderboard = null!; private PlayerArea? currentAudioSource; private readonly Room room; - private readonly MultiplayerRoomUser[] users; /// /// Creates a new . @@ -68,9 +70,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate : base(users.Select(u => u.UserID).ToArray()) { this.room = room; - this.users = users; instances = new PlayerArea[Users.Count]; + leaderboardProvider = new MultiSpectatorLeaderboardProvider(users); } [BackgroundDependencyLoader] @@ -133,25 +135,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate for (int i = 0; i < Users.Count; i++) grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock())); - LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users) - { - Expanded = { Value = true }, - }, _ => + LoadComponentAsync(leaderboardProvider, _ => { + AddInternal(leaderboardProvider); foreach (var instance in instances) - leaderboard.AddClock(instance.UserId, instance.SpectatorPlayerClock); + leaderboardProvider.AddClock(instance.UserId, instance.SpectatorPlayerClock); - leaderboardFlow.Insert(0, leaderboard); - - if (leaderboard.TeamScores.Count == 2) + if (leaderboardProvider.TeamScores.Count == 2) { LoadComponentAsync(new MatchScoreDisplay { - Team1Score = { BindTarget = leaderboard.TeamScores.First().Value }, - Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value }, + Team1Score = { BindTarget = leaderboardProvider.TeamScores.First().Value }, + Team2Score = { BindTarget = leaderboardProvider.TeamScores.Last().Value }, }, scoreDisplayContainer.Add); } }); + leaderboardFlow.Insert(0, new DrawableGameplayLeaderboard + { + Expanded = { Value = true } + }); LoadComponentAsync(new GameplayChatDisplay(room) { diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs similarity index 74% rename from osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs rename to osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index f6694505dc..85f5281bef 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; @@ -10,33 +11,39 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; -using osu.Game.Users; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public abstract partial class GameplayLeaderboard : CompositeDrawable + public partial class DrawableGameplayLeaderboard : CompositeDrawable { private readonly Cached sorting = new Cached(); public Bindable Expanded = new Bindable(); - protected readonly FillFlowContainer Flow; + protected readonly FillFlowContainer Flow; private bool requiresScroll; private readonly OsuScrollContainer scroll; - public GameplayLeaderboardScore? TrackedScore { get; private set; } + public DrawableGameplayLeaderboardScore? TrackedScore { get; private set; } + + [Resolved] + private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } + + private readonly IBindableList scores = new BindableList(); private const int max_panels = 8; /// /// Create a new leaderboard. /// - protected GameplayLeaderboard() + public DrawableGameplayLeaderboard() { - Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; + Width = DrawableGameplayLeaderboardScore.EXTENDED_WIDTH + DrawableGameplayLeaderboardScore.SHEAR_WIDTH; InternalChildren = new Drawable[] { @@ -44,10 +51,10 @@ namespace osu.Game.Screens.Play.HUD { ClampExtension = 0, RelativeSizeAxes = Axes.Both, - Child = Flow = new FillFlowContainer + Child = Flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, - X = GameplayLeaderboardScore.SHEAR_WIDTH, + X = DrawableGameplayLeaderboardScore.SHEAR_WIDTH, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(2.5f), @@ -62,22 +69,28 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); + if (leaderboardProvider != null) + { + scores.BindTo(leaderboardProvider.Scores); + scores.BindCollectionChanged((_, _) => + { + Clear(); + foreach (var score in scores) + Add(score); + }, true); + } + Scheduler.AddDelayed(sort, 1000, true); } /// /// Adds a player to the leaderboard. /// - /// The player. - /// - /// Whether the player should be tracked on the leaderboard. - /// Set to true for the local player or a player whose replay is currently being played. - /// - public ILeaderboardScore Add(IUser? user, bool isTracked) + public void Add(IGameplayLeaderboardScore score) { - var drawable = CreateLeaderboardScoreDrawable(user, isTracked); + var drawable = CreateLeaderboardScoreDrawable(score); - if (isTracked) + if (score.Tracked) { if (TrackedScore != null) throw new InvalidOperationException("Cannot track more than one score."); @@ -92,10 +105,8 @@ namespace osu.Game.Screens.Play.HUD drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true); int displayCount = Math.Min(Flow.Count, max_panels); - Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y); + Height = displayCount * (DrawableGameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y); requiresScroll = displayCount != Flow.Count; - - return drawable; } public void Clear() @@ -105,8 +116,8 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); } - protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) => - new GameplayLeaderboardScore(user, isTracked); + protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(IGameplayLeaderboardScore score) => + new DrawableGameplayLeaderboardScore(score); protected override void Update() { @@ -119,7 +130,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollTo(scrollTarget); } - const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT; + const float panel_height = DrawableGameplayLeaderboardScore.PANEL_HEIGHT; float fadeBottom = (float)(scroll.Current + scroll.DrawHeight); float fadeTop = (float)(scroll.Current + panel_height); @@ -171,14 +182,12 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < Flow.Count; i++) { Flow.SetLayoutPosition(orderedByScore[i], i); - orderedByScore[i].ScorePosition = CheckValidScorePosition(orderedByScore[i], i + 1) ? i + 1 : null; + orderedByScore[i].ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true ? null : i + 1; } sorting.Validate(); } - protected virtual bool CheckValidScorePosition(GameplayLeaderboardScore score, int position) => true; - private partial class InputDisabledScrollContainer : OsuScrollContainer { public InputDisabledScrollContainer() diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs similarity index 96% rename from osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs rename to osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index 3d46517a68..f04d3ee492 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -22,7 +22,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public partial class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore + public partial class DrawableGameplayLeaderboardScore : CompositeDrawable { public const float EXTENDED_WIDTH = regular_width + top_player_left_width_extension; @@ -112,19 +112,27 @@ namespace osu.Game.Screens.Play.HUD private bool isFriend; /// - /// Creates a new . + /// Creates a new . /// - /// The score's player. - /// Whether the player is the local user or a replay player. - public GameplayLeaderboardScore(IUser? user, bool tracked) + public DrawableGameplayLeaderboardScore(IGameplayLeaderboardScore score) { - User = user; - Tracked = tracked; + User = score.User; + Tracked = score.Tracked; + TotalScore.BindTo(score.TotalScore); + Accuracy.BindTo(score.Accuracy); + Combo.BindTo(score.Combo); + HasQuit.BindTo(score.HasQuit); + DisplayOrder.BindTo(score.DisplayOrder); + GetDisplayScore = score.GetDisplayScore; + + if (score.TeamColour != null) + { + BackgroundColour = score.TeamColour.Value; + TextColour = Color4.White; + } AutoSizeAxes = Axes.X; Height = PANEL_HEIGHT; - - GetDisplayScore = _ => TotalScore.Value; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs new file mode 100644 index 0000000000..20c7b16d79 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs @@ -0,0 +1,67 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Scoring; +using osu.Game.Users; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// Represents a score shown on a gameplay leaderboard. + /// The score is expected to update itself as gameplay progresses. + /// + public interface IGameplayLeaderboardScore + { + /// + /// The user playing. + /// + IUser User { get; } + + /// + /// Whether the score is being tracked. + /// Generally understood as true when this score is the score of the local user currently playing. + /// + bool Tracked { get; } + + /// + /// The current total of the score. + /// + BindableLong TotalScore { get; } + + /// + /// The current accuracy of the score. + /// + BindableDouble Accuracy { get; } + + /// + /// The current combo of the score. + /// + BindableInt Combo { get; } + + /// + /// Whether the user playing has quit. + /// + BindableBool HasQuit { get; } + + /// + /// An optional value to guarantee stable ordering. + /// Lower numbers will appear higher in cases of ties. + /// + Bindable DisplayOrder { get; } + + /// + /// A custom function which handles converting a score to a display score using a provide . + /// + /// + /// If no function is provided, will be used verbatim. + Func GetDisplayScore { get; set; } + + /// + /// The colour of the team that the user playing is on, if any. + /// + Colour4? TeamColour { get; } + } +} diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs deleted file mode 100644 index 1a5d7fd9a8..0000000000 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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.Bindables; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Screens.Play.HUD -{ - public interface ILeaderboardScore - { - BindableLong TotalScore { get; } - BindableDouble Accuracy { get; } - BindableInt Combo { get; } - - BindableBool HasQuit { get; } - - /// - /// An optional value to guarantee stable ordering. - /// Lower numbers will appear higher in cases of ties. - /// - Bindable DisplayOrder { get; } - - /// - /// A custom function which handles converting a score to a display score using a provide . - /// - /// - /// If no function is provided, will be used verbatim. - Func GetDisplayScore { set; } - } -} diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs deleted file mode 100644 index e9bb1d2101..0000000000 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ /dev/null @@ -1,108 +0,0 @@ -// 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.Bindables; -using osu.Framework.Graphics; -using osu.Game.Configuration; -using osu.Game.Online.API.Requests; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Select; -using osu.Game.Users; - -namespace osu.Game.Screens.Play.HUD -{ - public partial class SoloGameplayLeaderboard : GameplayLeaderboard - { - private const int duration = 100; - - private readonly Bindable configVisibility = new Bindable(); - - private readonly Bindable scoreSource = new Bindable(); - - private readonly IUser trackingUser; - - public readonly IBindableList Scores = new BindableList(); - - [Resolved] - private ScoreProcessor scoreProcessor { get; set; } = null!; - - /// - /// Whether the leaderboard should be visible regardless of the configuration value. - /// This is true by default, but can be changed. - /// - public readonly Bindable AlwaysVisible = new Bindable(true); - - public SoloGameplayLeaderboard(IUser trackingUser) - { - this.trackingUser = trackingUser; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); - config.BindWith(OsuSetting.BeatmapDetailTab, scoreSource); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true); - - // Alpha will be updated via `updateVisibility` below. - Alpha = 0; - - AlwaysVisible.BindValueChanged(_ => updateVisibility()); - configVisibility.BindValueChanged(_ => updateVisibility(), true); - } - - private void showScores() - { - Clear(); - - if (!Scores.Any()) - return; - - foreach (var s in Scores) - { - var score = Add(s.User, false); - - score.GetDisplayScore = s.GetDisplayScore; - score.TotalScore.Value = s.TotalScore; - score.Accuracy.Value = s.Accuracy; - score.Combo.Value = s.MaxCombo; - score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds(); - } - - ILeaderboardScore local = Add(trackingUser, true); - - local.GetDisplayScore = scoreProcessor.GetDisplayScore; - local.TotalScore.BindTarget = scoreProcessor.TotalScore; - local.Accuracy.BindTarget = scoreProcessor.Accuracy; - local.Combo.BindTarget = scoreProcessor.HighestCombo; - - // Local score should always show lower than any existing scores in cases of ties. - local.DisplayOrder.Value = long.MaxValue; - } - - protected override bool CheckValidScorePosition(GameplayLeaderboardScore score, int position) - { - // change displayed position to '-' when there are 50 already submitted scores and tracked score is last - if (score.Tracked && scoreSource.Value != PlayBeatmapDetailArea.TabType.Local) - { - if (position == Flow.Count && Flow.Count > GetScoresRequest.MAX_SCORES_PER_REQUEST) - return false; - } - - return base.CheckValidScorePosition(score, position); - } - - private void updateVisibility() => - this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b2e502406a..14bb1a1794 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -929,34 +929,30 @@ namespace osu.Game.Screens.Play #region Gameplay leaderboard + protected virtual bool ShowLeaderboard => false; + protected readonly Bindable LeaderboardExpandedState = new BindableBool(); private void loadLeaderboard() { + if (!ShowLeaderboard) + return; + HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState()); LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true); - var gameplayLeaderboard = CreateGameplayLeaderboard(); - - if (gameplayLeaderboard != null) + var gameplayLeaderboard = new DrawableGameplayLeaderboard(); + LoadComponentAsync(gameplayLeaderboard, leaderboard => { - LoadComponentAsync(gameplayLeaderboard, leaderboard => - { - if (!LoadedBeatmapSuccessfully) - return; + if (!LoadedBeatmapSuccessfully) + return; - leaderboard.Expanded.BindTo(LeaderboardExpandedState); + leaderboard.Expanded.BindTo(LeaderboardExpandedState); - AddLeaderboardToHUD(leaderboard); - }); - } + HUDOverlay.LeaderboardFlow.Add(leaderboard); + }); } - [CanBeNull] - protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null; - - protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard); - private void updateLeaderboardExpandedState() => LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a5952f3ff3..c997a67dea 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -8,18 +8,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Input.Bindings; -using osu.Game.Online.Leaderboards; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Screens.Play @@ -35,6 +33,9 @@ namespace osu.Game.Screens.Play private PlaybackSettings playbackSettings; + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); + protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo); private bool isAutoplayPlayback => GameplayState.Mods.OfType().Any(); @@ -48,6 +49,8 @@ namespace osu.Game.Screens.Play return base.CheckModsAllowFailure(); } + protected override bool ShowLeaderboard => true; + public ReplayPlayer(Score score, PlayerConfiguration configuration = null) : this((_, _) => score, configuration) { @@ -60,12 +63,6 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } - [Resolved] - private LeaderboardManager leaderboardManager { get; set; } = null!; - - private readonly IBindable globalScores = new Bindable(); - private readonly BindableList localScores = new BindableList(); - /// /// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings. /// @@ -82,6 +79,8 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; + AddInternal(leaderboardProvider); + playbackSettings = new PlaybackSettings { Depth = float.MaxValue, @@ -94,20 +93,6 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); } - protected override void LoadComplete() - { - base.LoadComplete(); - - globalScores.BindTo(leaderboardManager.Scores); - globalScores.BindValueChanged(_ => - { - localScores.Clear(); - - if (globalScores.Value is LeaderboardScores g) - localScores.AddRange(g.AllScores.OrderByTotalScore()); - }, true); - } - protected override void PrepareReplay() { DrawableRuleset?.SetReplayScore(Score); @@ -118,13 +103,6 @@ namespace osu.Game.Screens.Play // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; - protected override GameplayLeaderboard CreateGameplayLeaderboard() => - new SoloGameplayLeaderboard(Score.ScoreInfo.User) - { - AlwaysVisible = { Value = true }, - Scores = { BindTarget = localScores } - }; - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score) { // Only show the relevant button otherwise things look silly. diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index ed5dea98cd..e4e42e2f08 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -5,50 +5,34 @@ using System; using System.Diagnostics; -using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; -using osu.Game.Online.Leaderboards; using osu.Game.Online.Rooms; using osu.Game.Online.Solo; using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select.Leaderboards; namespace osu.Game.Screens.Play { public partial class SoloPlayer : SubmittingPlayer { - public SoloPlayer() - : this(null) - { - } + protected override bool ShowLeaderboard => true; - protected SoloPlayer(PlayerConfiguration configuration = null) + [Cached(typeof(IGameplayLeaderboardProvider))] + private readonly SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider(); + + public SoloPlayer([CanBeNull] PlayerConfiguration configuration = null) : base(configuration) { } - [Resolved] - private LeaderboardManager leaderboardManager { get; set; } = null!; - - private readonly IBindable globalScores = new Bindable(); - private readonly BindableList localScores = new BindableList(); - - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - - globalScores.BindTo(leaderboardManager.Scores); - globalScores.BindValueChanged(_ => - { - localScores.Clear(); - - if (globalScores.Value is LeaderboardScores g) - localScores.AddRange(g.AllScores.OrderByTotalScore()); - }, true); + AddInternal(leaderboardProvider); } protected override APIRequest CreateTokenRequest() @@ -65,30 +49,13 @@ namespace osu.Game.Screens.Play return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash); } - protected override GameplayLeaderboard CreateGameplayLeaderboard() => - new SoloGameplayLeaderboard(Score.ScoreInfo.User) - { - AlwaysVisible = { Value = false }, - Scores = { BindTarget = localScores } - }; - protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false; - protected override Task ImportScore(Score score) - { - // Before importing a score, stop binding the leaderboard with its score source. - // This avoids a case where the imported score may cause a leaderboard refresh - // (if the leaderboard's source is local). - globalScores.UnbindBindings(); - - return base.ImportScore(score); - } - protected override APIRequest CreateSubmissionRequest(Score score, long token) { - IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo; + IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo!; - Debug.Assert(beatmap!.OnlineID > 0); + Debug.Assert(beatmap.OnlineID > 0); return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID); } diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs new file mode 100644 index 0000000000..ba3e4f728b --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -0,0 +1,59 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Screens.Play.HUD; +using osu.Game.Users; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public class GameplayLeaderboardScore : IGameplayLeaderboardScore + { + public IUser User { get; } + public bool Tracked { get; } + public BindableLong TotalScore { get; } = new BindableLong(); + public BindableDouble Accuracy { get; } = new BindableDouble(); + public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); + public Bindable DisplayOrder { get; } = new BindableLong(); + public Func GetDisplayScore { get; set; } + public Colour4? TeamColour { get; init; } + + public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked) + { + User = user; + Tracked = tracked; + TotalScore.BindTarget = scoreProcessor.TotalScore; + Accuracy.BindTarget = scoreProcessor.Accuracy; + Combo.BindTarget = scoreProcessor.Combo; + GetDisplayScore = scoreProcessor.GetDisplayScore; + } + + public GameplayLeaderboardScore(IUser user, SpectatorScoreProcessor scoreProcessor, bool tracked) + { + User = user; + Tracked = tracked; + TotalScore.BindTarget = scoreProcessor.TotalScore; + Accuracy.BindTarget = scoreProcessor.Accuracy; + Combo.BindTarget = scoreProcessor.Combo; + GetDisplayScore = scoreProcessor.GetDisplayScore; + } + + public GameplayLeaderboardScore(ScoreInfo scoreInfo, bool tracked) + { + User = scoreInfo.User; + Tracked = tracked; + TotalScore.Value = scoreInfo.TotalScore; + Accuracy.Value = scoreInfo.Accuracy; + Combo.Value = scoreInfo.Combo; + DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); + GetDisplayScore = scoreInfo.GetDisplayScore; + } + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs new file mode 100644 index 0000000000..0138f855e2 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.Select.Leaderboards +{ + /// + /// Provides a leaderboard to show during gameplay. + /// + public interface IGameplayLeaderboardProvider + { + /// + /// List of all scores to display on the leaderboard. + /// + public IBindableList Scores { get; } + + /// + /// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores), + /// or is a full leaderboard (contains all scores that there will ever be). + /// + bool IsPartial { get; } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs similarity index 76% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs rename to osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs index ed92b719fc..19ae12a6ca 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiSpectatorLeaderboardProvider.cs @@ -4,13 +4,12 @@ using System; using osu.Framework.Timing; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Play.HUD; -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate +namespace osu.Game.Screens.Select.Leaderboards { - public partial class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard + public partial class MultiSpectatorLeaderboardProvider : MultiplayerLeaderboardProvider { - public MultiSpectatorLeaderboard(MultiplayerRoomUser[] users) + public MultiSpectatorLeaderboardProvider(MultiplayerRoomUser[] users) : base(users) { } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs similarity index 68% rename from osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs rename to osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index 922def6174..1c2b400164 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; @@ -20,20 +21,31 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; -using osu.Game.Users; +using osu.Game.Screens.Play.HUD; using osuTK.Graphics; -namespace osu.Game.Screens.Play.HUD +namespace osu.Game.Screens.Select.Leaderboards { [LongRunningLoad] - public partial class MultiplayerGameplayLeaderboard : GameplayLeaderboard + public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGameplayLeaderboardProvider { - protected readonly Dictionary UserScores = new Dictionary(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); + protected readonly Dictionary UserScores = new Dictionary(); public readonly SortedDictionary TeamScores = new SortedDictionary(); + public bool HasTeams => TeamScores.Count > 0; + + public bool IsPartial => false; + + private readonly MultiplayerRoomUser[] users; + + private readonly Bindable scoringMode = new Bindable(); + private readonly IBindableList playingUserIds = new BindableList(); + [Resolved] - private OsuColour colours { get; set; } = null!; + private UserLookupCache userLookupCache { get; set; } = null!; [Resolved] private SpectatorClient spectatorClient { get; set; } = null!; @@ -42,31 +54,19 @@ namespace osu.Game.Screens.Play.HUD private MultiplayerClient multiplayerClient { get; set; } = null!; [Resolved] - private UserLookupCache userLookupCache { get; set; } = null!; + private OsuColour colours { get; set; } = null!; - private Bindable scoringMode = null!; - - private readonly MultiplayerRoomUser[] playingUsers; - - private readonly IBindableList playingUserIds = new BindableList(); - - private bool hasTeams => TeamScores.Count > 0; - - /// - /// Construct a new leaderboard. - /// - /// IDs of all users in this match. - public MultiplayerGameplayLeaderboard(MultiplayerRoomUser[] users) + public MultiplayerLeaderboardProvider(MultiplayerRoomUser[] users) { - playingUsers = users; + this.users = users; } [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api, CancellationToken cancellationToken) { - scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + config.BindWith(OsuSetting.ScoreDisplayMode, scoringMode); - foreach (var user in playingUsers) + foreach (var user in users) { var scoreProcessor = new SpectatorScoreProcessor(user.UserID); scoreProcessor.Mode.BindTo(scoringMode); @@ -80,29 +80,29 @@ namespace osu.Game.Screens.Play.HUD TeamScores.Add(team, new BindableLong()); } - userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray(), cancellationToken) + userLookupCache.GetUsersAsync(users.Select(u => u.UserID).ToArray(), cancellationToken) .ContinueWith(task => { Schedule(() => { - var users = task.GetResultSafely(); + var lookedUpUsers = task.GetResultSafely(); - for (int i = 0; i < users.Length; i++) + for (int i = 0; i < lookedUpUsers.Length; i++) { - var user = users[i] ?? new APIUser + var user = lookedUpUsers[i] ?? new APIUser { - Id = playingUsers[i].UserID, + Id = users[i].UserID, Username = "Unknown user", }; var trackedUser = UserScores[user.Id]; - var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id); - leaderboardScore.GetDisplayScore = trackedUser.ScoreProcessor.GetDisplayScore; - leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy); - leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore); - leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo); - leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit); + var leaderboardScore = new GameplayLeaderboardScore(user, trackedUser.ScoreProcessor, user.Id == api.LocalUser.Value.Id) + { + HasQuit = { BindTarget = trackedUser.UserQuit }, + TeamColour = UserScores[user.OnlineID].Team is int team ? getTeamColour(team) : null, + }; + scores.Add(leaderboardScore); } }); }, cancellationToken); @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. - foreach (var user in playingUsers) + foreach (var user in users) { spectatorClient.WatchUser(user.UserID); @@ -127,34 +127,6 @@ namespace osu.Game.Screens.Play.HUD playingUserIds.BindCollectionChanged(playingUsersChanged); } - protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) - { - var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked); - - if (user != null) - { - if (UserScores[user.OnlineID].Team is int team) - { - leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f); - leaderboardScore.TextColour = Color4.White; - } - } - - return leaderboardScore; - } - - private Color4 getTeamColour(int team) - { - switch (team) - { - case 0: - return colours.TeamColourRed; - - default: - return colours.TeamColourBlue; - } - } - private void playingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -176,10 +148,10 @@ namespace osu.Game.Screens.Play.HUD private void updateTotals() { - if (!hasTeams) + if (!HasTeams) return; - foreach (var scores in TeamScores.Values) scores.Value = 0; + foreach (var teamTotal in TeamScores.Values) teamTotal.Value = 0; foreach (var u in UserScores.Values) { @@ -191,13 +163,25 @@ namespace osu.Game.Screens.Play.HUD } } + private Color4 getTeamColour(int team) + { + switch (team) + { + case 0: + return colours.TeamColourRed.Lighten(1.2f); + + default: + return colours.TeamColourBlue.Lighten(1.2f); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (spectatorClient.IsNotNull()) { - foreach (var user in playingUsers) + foreach (var user in users) spectatorClient.StopWatchingUser(user.UserID); } } diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs new file mode 100644 index 0000000000..125e8fdc9d --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -0,0 +1,41 @@ +// 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.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public partial class SoloGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider + { + public bool IsPartial { get; private set; } + + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); + + [BackgroundDependencyLoader] + private void load(LeaderboardManager leaderboardManager, GameplayState gameplayState) + { + var globalScores = leaderboardManager.Scores.Value; + + IsPartial = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; + + if (globalScores != null) + { + foreach (var topScore in globalScores.AllScores.OrderByTotalScore()) + scores.Add(new GameplayLeaderboardScore(topScore, false)); + } + + scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + { + // Local score should always show lower than any existing scores in cases of ties. + DisplayOrder = { Value = long.MaxValue } + }); + } + } +} From 1a68edfa58e34f1d3fa05f0f5e0b660c77efea10 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Apr 2025 03:30:53 +0900 Subject: [PATCH 237/281] Add failing test --- .../Multiplayer/TestSceneMultiplayer.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8066ea1b94..a8004f2685 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1056,6 +1056,45 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden))); } + [FlakyTest] + [Test] + public void TestGlobalBeatmapDoesNotChangeAtResults() + { + createRoom(() => new Room + { + Name = "Test Room", + QueueMode = QueueMode.AllPlayers, + Playlist = + [ + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod { Acronym = "HD" } }, + }, + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)).BeatmapInfo) + { + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod { Acronym = "HD" } }, + }, + ] + }); + + enterGameplay(); + + // Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out. + for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000) + { + double time = i; + AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType().SingleOrDefault()?.CurrentTime > time); + } + + AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); + + AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[0].BeatmapID)); + AddStep("return to match", () => multiplayerComponents.Exit()); + AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID)); + } + private void enterGameplay() { pressReadyButton(); From 6052fbb4f927a4562e55a1295da90d9d88618c22 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Apr 2025 03:03:49 +0900 Subject: [PATCH 238/281] Fix multiplayer background changing in results screen --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ac1c6cf22c..6d271a0077 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -467,7 +467,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { if (settings.PlaylistItemId != lastPlaylistItemId) { - updateGameplayState(); + Scheduler.AddOnce(updateGameplayState); lastPlaylistItemId = settings.PlaylistItemId; } @@ -480,7 +480,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onItemChanged(MultiplayerPlaylistItem item) { if (item.ID == client.Room?.Settings.PlaylistItemId) - updateGameplayState(); + Scheduler.AddOnce(updateGameplayState); } /// @@ -489,7 +489,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserStyleChanged(MultiplayerRoomUser user) { if (user.Equals(client.LocalUser)) - updateGameplayState(); + Scheduler.AddOnce(updateGameplayState); } /// @@ -498,7 +498,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onUserModsChanged(MultiplayerRoomUser user) { if (user.Equals(client.LocalUser)) - updateGameplayState(); + Scheduler.AddOnce(updateGameplayState); } /// From b71281bec8054d17484041b5ef6117ad5b8ee38f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Apr 2025 16:11:03 +0900 Subject: [PATCH 239/281] Fix osu!mania beatmap objects getting corrupted when updating beatmap background Closes https://github.com/ppy/osu/issues/32825. Tested manually to fix the issue. Setting up test coverage for this is going to likely take over an hour compared to the 30 second fix, so please advise if required. I couldn't find any existing tests which perform this flow. --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index cab6eddaa4..a1c81eedec 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -192,7 +192,7 @@ namespace osu.Game.Screens.Edit.Setup // note that this triggers a full save flow, including triggering a difficulty calculation. // this is not a cheap operation and should be reconsidered in the future. var beatmapWorking = beatmaps.GetWorkingBeatmap(b); - beatmaps.Save(b, beatmapWorking.Beatmap, beatmapWorking.GetSkin()); + beatmaps.Save(b, beatmapWorking.GetPlayableBeatmap(b.Ruleset), beatmapWorking.GetSkin()); } } From 67c6f8acdd44c221b507a69b51d7b9af6709cebc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Sep 2024 18:30:15 +0900 Subject: [PATCH 240/281] End high performance session when showing results screen --- osu.Game/Screens/Play/Player.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b2e502406a..eab964def7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -58,6 +58,11 @@ namespace osu.Game.Screens.Play public override bool AllowUserExit => false; // handled by HoldForMenuButton + /// + /// Raised after all gameplay has finished. + /// + public event Action OnShowingResults; + protected override bool PlayExitSound => !isRestarting; protected override UserActivity InitialActivity => new UserActivity.InSoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); @@ -873,6 +878,7 @@ namespace osu.Game.Screens.Play // This player instance may already be in the process of exiting. return; + OnShowingResults?.Invoke(); this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely())); }, Time.Current + delay, 50); From a15230eba189dadecc3434b7b15a52b8ceb82915 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Sep 2024 18:41:43 +0900 Subject: [PATCH 241/281] Centralise calls to end high performance sessions --- osu.Game/Screens/Play/PlayerLoader.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index bd4b62fd59..24d18a1610 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Threading; using System.Threading.Tasks; using ManagedBass.Fx; using osu.Framework.Allocation; @@ -302,8 +303,7 @@ namespace osu.Game.Screens.Play Debug.Assert(CurrentPlayer != null); - highPerformanceSession?.Dispose(); - highPerformanceSession = null; + endHighPerformance(); // prepare for a retry. CurrentPlayer = null; @@ -349,8 +349,7 @@ namespace osu.Game.Screens.Play BackgroundBrightnessReduction = false; Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); - highPerformanceSession?.Dispose(); - highPerformanceSession = null; + endHighPerformance(); return base.OnExiting(e); } @@ -587,7 +586,9 @@ namespace osu.Game.Screens.Play scheduledPushPlayer = Scheduler.AddDelayed(() => { // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). - var consumedPlayer = consumePlayer(); + Player consumedPlayer = consumePlayer(); + + consumedPlayer.OnShowingResults += endHighPerformance; ContentOut(); @@ -623,6 +624,8 @@ namespace osu.Game.Screens.Play scheduledPushPlayer = null; } + private void endHighPerformance() => Interlocked.Exchange(ref highPerformanceSession, null)?.Dispose(); + #region Disposal protected override void Dispose(bool isDisposing) @@ -635,8 +638,7 @@ namespace osu.Game.Screens.Play DisposalTask = LoadTask?.ContinueWith(_ => CurrentPlayer?.Dispose()); } - highPerformanceSession?.Dispose(); - highPerformanceSession = null; + endHighPerformance(); } #endregion From 1fad2a8f2cf632dbd71dfb2e9e5a93e3e41e9095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 10:05:57 +0200 Subject: [PATCH 242/281] Add failing test --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 9c72804a6b..6558834a63 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -215,6 +215,35 @@ namespace osu.Game.Tests.Scores.IO } } + [Test] + public void TestScoreWithInvalidModCombinationsWillNotImport() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + + var toImport = new ScoreInfo + { + User = new APIUser { Username = "Test user" }, + BeatmapInfo = beatmap.Beatmaps.First(), + Ruleset = new OsuRuleset().RulesetInfo, + ClientVersion = "12345", + Mods = new Mod[] { new OsuModHalfTime(), new OsuModDoubleTime() }, + }; + + Assert.Throws(() => LoadScoreIntoOsu(osu, toImport)); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportStatistics() { From 485c3e8e5385cd5004a2829cc1d45c25e4ea9d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 10:10:07 +0200 Subject: [PATCH 243/281] Refuse to import scores specifying incompatible mods Supersedes https://github.com/ppy/osu/pull/32817. The messaging of the failure to the user is maybe not the cleanest, but I'm not sure it's worth putting time in to improve it? --- osu.Game/Scoring/ScoreImporter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 69c53af16f..4b3f4a5e63 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -17,6 +17,7 @@ using osu.Game.Scoring.Legacy; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Utils; using Realms; namespace osu.Game.Scoring @@ -90,6 +91,9 @@ namespace osu.Game.Scoring ArgumentNullException.ThrowIfNull(model.BeatmapInfo); ArgumentNullException.ThrowIfNull(model.Ruleset); + if (!ModUtils.CheckCompatibleSet(model.Mods)) + throw new InvalidOperationException(@"The score specifies an incompatible set of mods!"); + if (string.IsNullOrEmpty(model.StatisticsJson)) model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); From a82bb5c2f6ae67ce9aa32656188bb286b5b4fef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 10:36:04 +0200 Subject: [PATCH 244/281] Add theoretically-valid-but-practically-not commented-out test cases --- osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs index 497d8a18b8..b70657815c 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayStability.cs @@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 5f, -19d, HitResult.Perfect }, new object[] { 5f, -19.2d, HitResult.Perfect }, new object[] { 5f, -19.38d, HitResult.Perfect }, + // new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues) new object[] { 5f, -19.44d, HitResult.Great }, new object[] { 5f, -19.7d, HitResult.Great }, new object[] { 5f, -20d, HitResult.Great }, @@ -69,6 +70,7 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { 9.3f, 14d, HitResult.Perfect }, new object[] { 9.3f, 14.2d, HitResult.Perfect }, new object[] { 9.3f, 14.6d, HitResult.Perfect }, + // new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues) new object[] { 9.3f, 14.7d, HitResult.Great }, new object[] { 9.3f, 15d, HitResult.Great }, new object[] { 9.3f, 35d, HitResult.Great }, From 5f4afe156fe6523f35029dfb1a7af225783db448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 11:37:28 +0200 Subject: [PATCH 245/281] Fix garbage data in test case --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 70f2fb1361..44f64365f0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -342,7 +342,6 @@ namespace osu.Game.Tests.Visual.SongSelect Mods = new Mod[] { new OsuModHidden(), - new OsuModHardRock(), new OsuModFlashlight { FollowDelay = { Value = 200 }, From 708d9ae1b013562bf5f7e3cfc290b05a57d4ac9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Apr 2025 19:21:33 +0900 Subject: [PATCH 246/281] Adjust `PlayerLoader` logic to avoid threading safety requirements --- osu.Game/Screens/Play/PlayerLoader.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 24d18a1610..d22717abd4 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -3,12 +3,12 @@ using System; using System.Diagnostics; -using System.Threading; using System.Threading.Tasks; using ManagedBass.Fx; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -564,6 +564,8 @@ namespace osu.Game.Screens.Play private void pushWhenLoaded() { + Debug.Assert(ThreadSafety.IsUpdateThread); + if (!this.IsCurrentScreen()) return; if (!readyForPush) @@ -624,7 +626,13 @@ namespace osu.Game.Screens.Play scheduledPushPlayer = null; } - private void endHighPerformance() => Interlocked.Exchange(ref highPerformanceSession, null)?.Dispose(); + private void endHighPerformance() + { + Debug.Assert(ThreadSafety.IsUpdateThread); + + highPerformanceSession?.Dispose(); + highPerformanceSession = null; + } #region Disposal @@ -638,7 +646,8 @@ namespace osu.Game.Screens.Play DisposalTask = LoadTask?.ContinueWith(_ => CurrentPlayer?.Dispose()); } - endHighPerformance(); + // This is only a failsafe; should be disposed more immediately by `endHighPerformance` call. + highPerformanceSession?.Dispose(); } #endregion From 47d943afd78fa3015685a8bc85969b2e622a9e57 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 17 Apr 2025 20:05:34 +0900 Subject: [PATCH 247/281] Fix incorrect assertion message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index a8004f2685..03fe9b8b58 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1092,7 +1092,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[0].BeatmapID)); AddStep("return to match", () => multiplayerComponents.Exit()); - AddAssert("global beatmap still matches first playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID)); + AddAssert("global beatmap matches second playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID)); } private void enterGameplay() From 9e2a05a1fb423b9a5a7cb173da48c958f1f46ded Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:21:32 -0400 Subject: [PATCH 248/281] Update song select panel metrics in line with standard specifications and apply minor adjustments --- .../Drawables/DifficultySpectrumDisplay.cs | 2 +- osu.Game/Graphics/Carousel/CarouselItem.cs | 2 +- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 13 +++++----- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 13 +++++----- .../SelectV2/PanelBeatmapStandalone.cs | 24 ++++++++----------- .../SelectV2/PanelUpdateBeatmapButton.cs | 6 ++--- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index fc41c7c6dc..b7f4d4ca61 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -141,7 +141,7 @@ namespace osu.Game.Beatmaps.Drawables Add(countText = new OsuSpriteText { - Font = OsuFont.Default.With(size: 12), + Font = OsuFont.Style.Caption1, Anchor = Anchor.Centre, Origin = Anchor.Centre, Padding = new MarginPadding { Bottom = 1 } diff --git a/osu.Game/Graphics/Carousel/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs index 223c8d9869..47e83beca6 100644 --- a/osu.Game/Graphics/Carousel/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -11,7 +11,7 @@ namespace osu.Game.Graphics.Carousel /// public sealed class CarouselItem : IComparable { - public const float DEFAULT_HEIGHT = 50; + public const float DEFAULT_HEIGHT = 45; /// /// The model this item is representing. diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 6742577389..c8ae443364 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.SelectV2 Icon = difficultyIcon = new ConstrainedIconContainer { - Size = new Vector2(20), + Size = new Vector2(16f), Margin = new MarginPadding { Horizontal = 5f }, Colour = colourProvider.Background5, }; @@ -100,12 +100,13 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Scale = new Vector2(0.875f), }, localRank = new PanelLocalRankDisplay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Scale = new Vector2(0.75f) + Scale = new Vector2(0.65f) }, starCounter = new StarCounter { @@ -123,22 +124,22 @@ namespace osu.Game.Screens.SelectV2 { keyCountText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Alpha = 0, }, difficultyText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Right = 8f }, + Margin = new MarginPadding { Right = 5f }, }, authorText = new OsuSpriteText { Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 179d4d6444..7f5aa6ffe8 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class PanelBeatmapSet : Panel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.7f; private PanelSetBackground background = null!; @@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.Solid.ChevronRight, - Size = new Vector2(12), + Size = new Vector2(8), X = 1f, Colour = colourProvider.Background5, }, @@ -77,17 +77,17 @@ namespace osu.Game.Screens.SelectV2 { titleText = new OsuSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Font = OsuFont.Style.Heading1.With(typeface: Typeface.TorusAlternate), }, artistText = new OsuSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }, new FillFlowContainer { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, + Margin = new MarginPadding { Top = 4f }, Children = new Drawable[] { updateButton = new PanelUpdateBeatmapButton @@ -100,8 +100,7 @@ namespace osu.Game.Screens.SelectV2 { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + TextSize = OsuFont.Style.Caption2.Size, Margin = new MarginPadding { Right = 5f }, }, difficultiesDisplay = new DifficultySpectrumDisplay diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index a0d7484587..a90a84d115 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2 { public partial class PanelBeatmapStandalone : Panel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.7f; [Resolved] private IBindable ruleset { get; set; } = null!; @@ -76,7 +76,7 @@ namespace osu.Game.Screens.SelectV2 Icon = difficultyIcon = new ConstrainedIconContainer { - Size = new Vector2(20), + Size = new Vector2(16), Margin = new MarginPadding { Horizontal = 5f }, Colour = colourProvider.Background5, }; @@ -95,19 +95,16 @@ namespace osu.Game.Screens.SelectV2 { titleText = new OsuSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, + Font = OsuFont.Style.Heading1.With(typeface: Typeface.TorusAlternate), }, artistText = new OsuSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), }, new FillFlowContainer { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5f }, Children = new Drawable[] { updateButton = new PanelUpdateBeatmapButton @@ -120,8 +117,7 @@ namespace osu.Game.Screens.SelectV2 { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + TextSize = OsuFont.Style.Caption2.Size, Margin = new MarginPadding { Right = 5f }, }, difficultyLine = new FillFlowContainer @@ -134,19 +130,19 @@ namespace osu.Game.Screens.SelectV2 { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Scale = new Vector2(8f / 9f), + Scale = new Vector2(0.875f), Margin = new MarginPadding { Right = 5f }, }, difficultyRank = new PanelLocalRankDisplay { - Scale = new Vector2(8f / 11), + Scale = new Vector2(0.65f), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Margin = new MarginPadding { Right = 5f }, }, difficultyKeyCountText = new OsuSpriteText { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Heading2, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Alpha = 0, @@ -154,7 +150,7 @@ namespace osu.Game.Screens.SelectV2 }, difficultyName = new OsuSpriteText { - Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Heading2, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Margin = new MarginPadding { Right = 5f, Bottom = 2f }, @@ -162,7 +158,7 @@ namespace osu.Game.Screens.SelectV2 difficultyAuthor = new OsuSpriteText { Colour = colourProvider.Content2, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Margin = new MarginPadding { Right = 5f, Bottom = 2f }, diff --git a/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs index 2a850321a6..4c767df9d8 100644 --- a/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs +++ b/osu.Game/Screens/SelectV2/PanelUpdateBeatmapButton.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.SelectV2 public PanelUpdateBeatmapButton() { - Size = new Vector2(75f, 22f); + Size = new Vector2(72, 22f); } private Bindable preferNoVideo = null!; @@ -63,7 +63,7 @@ namespace osu.Game.Screens.SelectV2 [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - const float icon_size = 14; + const float icon_size = 12; preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); @@ -110,7 +110,7 @@ namespace osu.Game.Screens.SelectV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.Default.With(weight: FontWeight.Bold), + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Text = "Update", } } From 144cec14682baba5b8397c4e9a03df150047ff27 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:23:33 -0400 Subject: [PATCH 249/281] Add test cases to visualise rank display in panels --- .../SongSelectV2/TestScenePanelBeatmap.cs | 19 +++++++++++++++++++ .../TestScenePanelBeatmapStandalone.cs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs index 53a1355fc2..c0a77553c2 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmap.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.UserInterface; @@ -66,6 +72,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); } + [Test] + public void TestLocalRank() + { + foreach (var rank in Enum.GetValues()) + { + AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType().ForEach(p => + { + p.Show(); + p.Rank = rank; + })); + } + } + protected override Drawable CreateContent() { return new FillFlowContainer diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs index 4adee17868..93e495320f 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelBeatmapStandalone.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual.UserInterface; @@ -66,6 +72,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); } + [Test] + public void TestLocalRank() + { + foreach (var rank in Enum.GetValues()) + { + AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType().ForEach(p => + { + p.Show(); + p.Rank = rank; + })); + } + } + protected override Drawable CreateContent() { return new FillFlowContainer From d546bbaf8f25bbcf3f74221e0a4ec04d5a781acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 17 Apr 2025 14:26:34 +0200 Subject: [PATCH 250/281] Attempt to fix tests --- .../MultiplayerGameplayLeaderboardTestScene.cs | 17 +++++++++++++---- .../SoloGameplayLeaderboardProvider.cs | 17 ++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 644b7f522e..1481629ba0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -147,10 +147,19 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for load", () => Leaderboard!.IsLoaded); - AddStep("check watch requests were sent", () => + AddUntilStep("check watch requests were sent", () => { - foreach (var user in MultiplayerUsers) - spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once); + try + { + foreach (var user in MultiplayerUsers) + spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once); + + return true; + } + catch (MockException) + { + return false; + } }); } @@ -181,7 +190,7 @@ namespace osu.Game.Tests.Visual.Multiplayer spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once); return true; } - catch + catch (MockException) { return false; } diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 125e8fdc9d..216fda8d9f 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -19,11 +19,11 @@ namespace osu.Game.Screens.Select.Leaderboards private readonly BindableList scores = new BindableList(); [BackgroundDependencyLoader] - private void load(LeaderboardManager leaderboardManager, GameplayState gameplayState) + private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) { - var globalScores = leaderboardManager.Scores.Value; + var globalScores = leaderboardManager?.Scores.Value; - IsPartial = leaderboardManager.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; + IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; if (globalScores != null) { @@ -31,11 +31,14 @@ namespace osu.Game.Screens.Select.Leaderboards scores.Add(new GameplayLeaderboardScore(topScore, false)); } - scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + if (gameplayState != null) { - // Local score should always show lower than any existing scores in cases of ties. - DisplayOrder = { Value = long.MaxValue } - }); + scores.Add(new GameplayLeaderboardScore(gameplayState.Score.ScoreInfo.User, gameplayState.ScoreProcessor, true) + { + // Local score should always show lower than any existing scores in cases of ties. + DisplayOrder = { Value = long.MaxValue } + }); + } } } } From 8e3bace2721ca9ec66978f1ab20675bbee143608 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:03 -0400 Subject: [PATCH 251/281] Add general constants in `SongSelect` --- osu.Game/Screens/SelectV2/SongSelect.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 67ca110dab..ca09b2a40a 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -22,6 +22,10 @@ namespace osu.Game.Screens.SelectV2 { private const float logo_scale = 0.4f; + public const float WEDGE_CONTENT_MARGIN = CORNER_RADIUS_HIDE_OFFSET + OsuGame.SCREEN_EDGE_MARGIN; + public const float CORNER_RADIUS_HIDE_OFFSET = 20f; + public const float ENTER_DURATION = 600; + private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine) { ShowPresets = true, From 89a8c50a45afcd6893fb2e06f48e07e66d6b80fc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:11 -0400 Subject: [PATCH 252/281] Add `WedgeBackground` --- osu.Game/Screens/SelectV2/WedgeBackground.cs | 54 ++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 osu.Game/Screens/SelectV2/WedgeBackground.cs diff --git a/osu.Game/Screens/SelectV2/WedgeBackground.cs b/osu.Game/Screens/SelectV2/WedgeBackground.cs new file mode 100644 index 0000000000..ecfbd51260 --- /dev/null +++ b/osu.Game/Screens/SelectV2/WedgeBackground.cs @@ -0,0 +1,54 @@ +// 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.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; + +namespace osu.Game.Screens.SelectV2 +{ + internal partial class WedgeBackground : CompositeDrawable + { + public float StartAlpha { get; init; } = 0.9f; + + public float FinalAlpha { get; init; } = 0.6f; + + public float WidthForGradient { get; init; } = 0.3f; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0.6f, + Alpha = 0.5f, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background2, colourProvider.Background2.Opacity(0)), + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Width = 1 - WidthForGradient, + Colour = colourProvider.Background5.Opacity(StartAlpha), + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = WidthForGradient, + Colour = ColourInfo.GradientHorizontal(colourProvider.Background5.Opacity(StartAlpha), colourProvider.Background5.Opacity(FinalAlpha)), + }, + }; + } + } +} From 10c421682af3e11f03451c471cd180cb127bc98a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 08:15:59 -0400 Subject: [PATCH 253/281] Add popover layer in test scene base class and use half width by default --- .../SongSelectComponentsTestScene.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 9e9cd3505a..87c96763d5 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Game.Graphics.Cursor; using osu.Game.Overlays; @@ -27,18 +28,19 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [BackgroundDependencyLoader] private void load() { - base.Content.Child = resizeContainer = new Container + base.Content.Child = new PopoverContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Width = relativeWidth, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = resizeContainer = new Container { - Content + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = relativeWidth, + Child = Content } }; - AddSliderStep("change relative width", 0, 1f, 1f, v => + AddSliderStep("change relative width", 0, 1f, 0.5f, v => { if (resizeContainer != null) resizeContainer.Width = v; From bfe8cc47ecd6f4758965ddc23eb8ab7690062e86 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:37:50 -0400 Subject: [PATCH 254/281] Introduce customisation properties to base song select test scene --- .../Visual/SongSelectV2/SongSelectComponentsTestScene.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs index 87c96763d5..f86ca869e1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectComponentsTestScene.cs @@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private Container? resizeContainer; private float relativeWidth; + protected virtual Anchor ComponentAnchor => Anchor.TopLeft; + protected virtual float InitialRelativeWidth => 0.5f; + [BackgroundDependencyLoader] private void load() { @@ -33,6 +36,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 RelativeSizeAxes = Axes.Both, Child = resizeContainer = new Container { + Anchor = ComponentAnchor, + Origin = ComponentAnchor, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Width = relativeWidth, @@ -40,7 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } }; - AddSliderStep("change relative width", 0, 1f, 0.5f, v => + AddSliderStep("change relative width", 0, 1f, InitialRelativeWidth, v => { if (resizeContainer != null) resizeContainer.Width = v; From f93e731a5556a541b8c0fb4fb888492a1232720c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 08:51:53 -0400 Subject: [PATCH 255/281] Adjust sheared dropdown menu padding --- .../UserInterfaceV2/ShearedDropdown.cs | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs index 609f77dd7e..d77b9be2da 100644 --- a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -36,16 +36,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - protected override void Update() - { - base.Update(); - - var header = (ShearedDropdownHeader)Header; - var menu = (ShearedDropdownMenu)Menu; - - menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 10f, Right = 6f }; - } - public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; @@ -62,16 +52,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu { - public new MarginPadding Padding - { - get => base.Padding; - set => base.Padding = value; - } - public ShearedDropdownMenu() { Shear = OsuGame.SHEAR; Margin = new MarginPadding { Top = 5f }; + Padding = new MarginPadding + { + Left = -6f, + Right = 6f + }; } protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item) @@ -92,8 +81,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public partial class ShearedDropdownHeader : DropdownHeader { - private const float corner_radius = 5f; - private LocalisableString label; protected override LocalisableString Label @@ -127,7 +114,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public ShearedDropdownHeader() { Shear = OsuGame.SHEAR; - CornerRadius = corner_radius; + CornerRadius = ShearedButton.CORNER_RADIUS; Masking = true; Foreground.Children = new Drawable[] @@ -148,7 +135,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { LabelContainer = new Container { - CornerRadius = corner_radius, + Depth = float.MaxValue, + CornerRadius = ShearedButton.CORNER_RADIUS, Masking = true, AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -159,8 +147,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, labelText = new OsuSpriteText { - Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f }, - Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + Margin = new MarginPadding + { + Horizontal = 10f, + // Chosen specifically so the height of these dropdowns matches ShearedToggleButton (30). + Vertical = 7f + }, + Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold), Shear = -OsuGame.SHEAR, }, }, @@ -180,7 +173,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Padding = new MarginPadding { Right = 15f }, - Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + Font = OsuFont.Style.Body, RelativeSizeAxes = Axes.X, }, chevron = new SpriteIcon @@ -197,8 +190,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 } }, }; - - AddInternal(LabelContainer.CreateProxy()); } [BackgroundDependencyLoader] @@ -223,7 +214,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth }; // By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it. - Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - corner_radius }; + Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - ShearedButton.CORNER_RADIUS }; } protected override bool OnHover(HoverEvent e) From a6a8e2a44fb410f3382b0ddb7f4a2b2b777a38b3 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:36:03 -0400 Subject: [PATCH 256/281] Move collection dropdown test coverage to isolated test scene --- .../TestSceneCollectionDropdown.cs | 271 ++++++++++++++++++ .../SongSelect/TestSceneFilterControl.cs | 252 +--------------- 2 files changed, 272 insertions(+), 251 deletions(-) create mode 100644 osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs new file mode 100644 index 0000000000..a47f3c5108 --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs @@ -0,0 +1,271 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Tests.Resources; +using osuTK.Input; +using Realms; + +namespace osu.Game.Tests.Visual.Collections +{ + public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene + { + private BeatmapManager beatmapManager = null!; + private CollectionDropdown dropdown = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + writeAndRefresh(r => r.RemoveAll()); + + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = dropdown = new CollectionDropdown + { + Width = 300, + Y = 100, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + }; + }); + + [Test] + public void TestEmptyCollectionFilterContainsAllBeatmaps() + { + assertCollectionDropdownContains("All beatmaps"); + assertCollectionHeaderDisplays("All beatmaps"); + } + + [Test] + public void TestCollectionAddedToDropdown() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + assertCollectionDropdownContains("1"); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionsCleared() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); + + AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + + AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); + + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestCollectionRemovedFromDropdown() + { + BeatmapCollection first = null!; + + AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first))); + + assertCollectionDropdownContains("1", false); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionRenamed() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1)); + + addExpandHeaderStep(); + + AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First")); + + assertCollectionDropdownContains("First"); + assertCollectionHeaderDisplays("First"); + } + + [Test] + public void TestAllBeatmapFilterDoesNotHaveAddButton() + { + addExpandHeaderStep(); + AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0))); + AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent); + } + + [Test] + public void TestCollectionFilterHasAddButton() + { + addExpandHeaderStep(); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1))); + AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent); + } + + [Test] + public void TestButtonDisabledAndEnabledWithBeatmapChanges() + { + addExpandHeaderStep(); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); + + AddStep("set dummy beatmap", () => Beatmap.SetDefault()); + AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); + } + + [Test] + public void TestButtonChangesWhenAddedAndRemovedFromCollection() + { + addExpandHeaderStep(); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + + AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash))); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); + + AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear())); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + } + + [Test] + public void TestButtonAddsAndRemovesBeatmap() + { + addExpandHeaderStep(); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + } + + [Test] + public void TestManageCollectionsFilterIsNotSelected() + { + bool received = false; + + addExpandHeaderStep(); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List { "abc" })))); + assertCollectionDropdownContains("1"); + + AddStep("select collection", () => + { + InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); + InputManager.Click(MouseButton.Left); + }); + + addExpandHeaderStep(); + + AddStep("watch for filter requests", () => + { + received = false; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + }); + + AddStep("click manage collections filter", () => + { + int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; + InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1"); + + AddAssert("filter request not fired", () => !received); + } + + private void writeAndRefresh(Action action) => Realm.Write(r => + { + action(r); + r.Refresh(); + }); + + private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); + + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) + => AddUntilStep($"collection dropdown header displays '{collectionName}'", + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); + + private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); + + private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => + AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", + // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 + () => shouldContain == dropdown.ChildrenOfType().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); + + private IconButton getAddOrRemoveButton(int index) + => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); + + private void addExpandHeaderStep() => AddStep("expand header", () => + { + InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () => + { + InputManager.MoveMouseTo(getAddOrRemoveButton(index)); + InputManager.Click(MouseButton.Left); + }); + + private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) + { + // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 + CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); + return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index a639d50eee..41e44357d7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -1,57 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Platform; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Collections; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; using osu.Game.Screens.Select; -using osu.Game.Tests.Resources; -using osuTK.Input; -using Realms; namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneFilterControl : OsuManualInputManagerTestScene { - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - - private BeatmapManager beatmapManager = null!; - private FilterControl control = null!; - - [BackgroundDependencyLoader] - private void load(GameHost host) - { - Dependencies.Cache(new RealmRulesetStore(Realm)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); - Dependencies.Cache(Realm); - - beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); - - base.Content.AddRange(new Drawable[] - { - Content - }); - } - [SetUp] public void SetUp() => Schedule(() => { - writeAndRefresh(r => r.RemoveAll()); - - Child = control = new FilterControl + Child = new FilterControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -59,216 +20,5 @@ namespace osu.Game.Tests.Visual.SongSelect Height = FilterControl.HEIGHT, }; }); - - [Test] - public void TestEmptyCollectionFilterContainsAllBeatmaps() - { - assertCollectionDropdownContains("All beatmaps"); - assertCollectionHeaderDisplays("All beatmaps"); - } - - [Test] - public void TestCollectionAddedToDropdown() - { - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); - assertCollectionDropdownContains("1"); - assertCollectionDropdownContains("2"); - } - - [Test] - public void TestCollectionsCleared() - { - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); - - AddAssert("check count 5", () => control.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); - - AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - - AddAssert("check count 2", () => control.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); - } - - [Test] - public void TestCollectionRemovedFromDropdown() - { - BeatmapCollection first = null!; - - AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1")))); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); - AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first))); - - assertCollectionDropdownContains("1", false); - assertCollectionDropdownContains("2"); - } - - [Test] - public void TestCollectionRenamed() - { - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - AddStep("select collection", () => - { - var dropdown = control.ChildrenOfType().Single(); - dropdown.Current.Value = dropdown.ItemSource.ElementAt(1); - }); - - addExpandHeaderStep(); - - AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First")); - - assertCollectionDropdownContains("First"); - assertCollectionHeaderDisplays("First"); - } - - [Test] - public void TestAllBeatmapFilterDoesNotHaveAddButton() - { - addExpandHeaderStep(); - AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0))); - AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent); - } - - [Test] - public void TestCollectionFilterHasAddButton() - { - addExpandHeaderStep(); - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1))); - AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent); - } - - [Test] - public void TestButtonDisabledAndEnabledWithBeatmapChanges() - { - addExpandHeaderStep(); - - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - - AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); - AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); - - AddStep("set dummy beatmap", () => Beatmap.SetDefault()); - AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); - } - - [Test] - public void TestButtonChangesWhenAddedAndRemovedFromCollection() - { - addExpandHeaderStep(); - - AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); - - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - - assertFirstButtonIs(FontAwesome.Solid.PlusSquare); - - AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash))); - assertFirstButtonIs(FontAwesome.Solid.MinusSquare); - - AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear())); - assertFirstButtonIs(FontAwesome.Solid.PlusSquare); - } - - [Test] - public void TestButtonAddsAndRemovesBeatmap() - { - addExpandHeaderStep(); - - AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); - - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); - assertCollectionDropdownContains("1"); - assertFirstButtonIs(FontAwesome.Solid.PlusSquare); - - addClickAddOrRemoveButtonStep(1); - AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); - assertFirstButtonIs(FontAwesome.Solid.MinusSquare); - - addClickAddOrRemoveButtonStep(1); - AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); - assertFirstButtonIs(FontAwesome.Solid.PlusSquare); - } - - [Test] - public void TestManageCollectionsFilterIsNotSelected() - { - bool received = false; - - addExpandHeaderStep(); - - AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List { "abc" })))); - assertCollectionDropdownContains("1"); - - AddStep("select collection", () => - { - InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); - InputManager.Click(MouseButton.Left); - }); - - addExpandHeaderStep(); - - AddStep("watch for filter requests", () => - { - received = false; - control.ChildrenOfType().First().RequestFilter = () => received = true; - }); - - AddStep("click manage collections filter", () => - { - int lastItemIndex = control.ChildrenOfType().Single().Items.Count() - 1; - InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); - InputManager.Click(MouseButton.Left); - }); - - AddAssert("collection filter still selected", () => control.CreateCriteria().CollectionBeatmapMD5Hashes?.Any() == true); - - AddAssert("filter request not fired", () => !received); - } - - private void writeAndRefresh(Action action) => Realm.Write(r => - { - action(r); - r.Refresh(); - }); - - private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); - - private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) - => AddUntilStep($"collection dropdown header displays '{collectionName}'", - () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().First().Text == collectionName)); - - private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); - - private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => - AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", - // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 - () => shouldContain == control.ChildrenOfType().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); - - private IconButton getAddOrRemoveButton(int index) - => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); - - private void addExpandHeaderStep() => AddStep("expand header", () => - { - InputManager.MoveMouseTo(control.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - - private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () => - { - InputManager.MoveMouseTo(getAddOrRemoveButton(index)); - InputManager.Click(MouseButton.Left); - }); - - private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) - { - // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 - CollectionFilterMenuItem item = control.ChildrenOfType().Single().ItemSource.ElementAt(index); - return control.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); - } } } From 54c13937af6e06cc1bc234f88627be80a52dad9a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:36:14 -0400 Subject: [PATCH 257/281] Add sheared collection dropdown --- .../TestSceneShearedCollectionDropdown.cs | 271 ++++++++++++++++++ .../Collections/ShearedCollectionDropdown.cs | 270 +++++++++++++++++ 2 files changed, 541 insertions(+) create mode 100644 osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs create mode 100644 osu.Game/Collections/ShearedCollectionDropdown.cs diff --git a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs b/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs new file mode 100644 index 0000000000..f1afdf2019 --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs @@ -0,0 +1,271 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Tests.Resources; +using osuTK.Input; +using Realms; + +namespace osu.Game.Tests.Visual.Collections +{ + public partial class TestSceneShearedCollectionDropdown : OsuManualInputManagerTestScene + { + private BeatmapManager beatmapManager = null!; + private ShearedCollectionDropdown dropdown = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + writeAndRefresh(r => r.RemoveAll()); + + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = dropdown = new ShearedCollectionDropdown + { + Width = 300, + Y = 100, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + }; + }); + + [Test] + public void TestEmptyCollectionFilterContainsAllBeatmaps() + { + assertCollectionDropdownContains("All beatmaps"); + assertCollectionHeaderDisplays("All beatmaps"); + } + + [Test] + public void TestCollectionAddedToDropdown() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + assertCollectionDropdownContains("1"); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionsCleared() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); + + AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + + AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); + + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestCollectionRemovedFromDropdown() + { + BeatmapCollection first = null!; + + AddStep("add collection", () => writeAndRefresh(r => r.Add(first = new BeatmapCollection(name: "1")))); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); + AddStep("remove collection", () => writeAndRefresh(r => r.Remove(first))); + + assertCollectionDropdownContains("1", false); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionRenamed() + { + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + AddStep("select collection", () => dropdown.Current.Value = dropdown.ItemSource.ElementAt(1)); + + addExpandHeaderStep(); + + AddStep("change name", () => writeAndRefresh(_ => getFirstCollection().Name = "First")); + + assertCollectionDropdownContains("First"); + assertCollectionHeaderDisplays("First"); + } + + [Test] + public void TestAllBeatmapFilterDoesNotHaveAddButton() + { + addExpandHeaderStep(); + AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0))); + AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent); + } + + [Test] + public void TestCollectionFilterHasAddButton() + { + addExpandHeaderStep(); + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1))); + AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent); + } + + [Test] + public void TestButtonDisabledAndEnabledWithBeatmapChanges() + { + addExpandHeaderStep(); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); + + AddStep("set dummy beatmap", () => Beatmap.SetDefault()); + AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); + } + + [Test] + public void TestButtonChangesWhenAddedAndRemovedFromCollection() + { + addExpandHeaderStep(); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + + AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash))); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); + + AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear())); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + } + + [Test] + public void TestButtonAddsAndRemovesBeatmap() + { + addExpandHeaderStep(); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); + assertCollectionDropdownContains("1"); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); + } + + [Test] + public void TestManageCollectionsFilterIsNotSelected() + { + bool received = false; + + addExpandHeaderStep(); + + AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1", new List { "abc" })))); + assertCollectionDropdownContains("1"); + + AddStep("select collection", () => + { + InputManager.MoveMouseTo(getCollectionDropdownItemAt(1)); + InputManager.Click(MouseButton.Left); + }); + + addExpandHeaderStep(); + + AddStep("watch for filter requests", () => + { + received = false; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + }); + + AddStep("click manage collections filter", () => + { + int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; + InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("collection filter still selected", () => dropdown.Current.Value.CollectionName == "1"); + + AddAssert("filter request not fired", () => !received); + } + + private void writeAndRefresh(Action action) => Realm.Write(r => + { + action(r); + r.Refresh(); + }); + + private BeatmapCollection getFirstCollection() => Realm.Run(r => r.All().First()); + + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) + => AddUntilStep($"collection dropdown header displays '{collectionName}'", + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); + + private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); + + private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => + AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", + // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 + () => shouldContain == dropdown.ChildrenOfType().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)); + + private IconButton getAddOrRemoveButton(int index) + => getCollectionDropdownItemAt(index).ChildrenOfType().Single(); + + private void addExpandHeaderStep() => AddStep("expand header", () => + { + InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () => + { + InputManager.MoveMouseTo(getAddOrRemoveButton(index)); + InputManager.Click(MouseButton.Left); + }); + + private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) + { + // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 + CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); + return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); + } + } +} diff --git a/osu.Game/Collections/ShearedCollectionDropdown.cs b/osu.Game/Collections/ShearedCollectionDropdown.cs new file mode 100644 index 0000000000..2bb2f5bfe7 --- /dev/null +++ b/osu.Game/Collections/ShearedCollectionDropdown.cs @@ -0,0 +1,270 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; +using Realms; + +namespace osu.Game.Collections +{ + /// + /// A dropdown to select the collection to be used to filter results. + /// + public partial class ShearedCollectionDropdown : ShearedDropdown + { + /// + /// Whether to show the "manage collections..." menu item in the dropdown. + /// + protected virtual bool ShowManageCollectionsItem => true; + + public Action? RequestFilter { private get; set; } + + private readonly BindableList filters = new BindableList(); + + [Resolved] + private ManageCollectionsDialog? manageCollectionsDialog { get; set; } + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + private IDisposable? realmSubscription; + + private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem(); + + public ShearedCollectionDropdown() + : base("Collection") + { + ItemSource = filters; + + Current.Value = allBeatmapsItem; + AlwaysShowSearchBar = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + realmSubscription = realm.RegisterForNotifications(r => r.All().OrderBy(c => c.Name), collectionsChanged); + + Current.BindValueChanged(selectionChanged); + } + + private void collectionsChanged(IRealmCollection collections, ChangeSet? changes) + { + if (changes == null) + { + filters.Clear(); + filters.Add(allBeatmapsItem); + filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm)))); + if (ShowManageCollectionsItem) + filters.Add(new ManageCollectionsFilterMenuItem()); + } + else + { + foreach (int i in changes.DeletedIndices.OrderDescending()) + filters.RemoveAt(i + 1); + + foreach (int i in changes.InsertedIndices) + filters.Insert(i + 1, new CollectionFilterMenuItem(collections[i].ToLive(realm))); + + var selectedItem = SelectedItem?.Value; + + foreach (int i in changes.NewModifiedIndices) + { + var updatedItem = collections[i]; + + // This is responsible for updating the state of the +/- button and the collection's name. + // TODO: we can probably make the menu items update with changes to avoid this. + filters.RemoveAt(i + 1); + filters.Insert(i + 1, new CollectionFilterMenuItem(updatedItem.ToLive(realm))); + + if (updatedItem.ID == selectedItem?.Collection?.ID) + { + // This current update and schedule is required to work around dropdown headers not updating text even when the selected item + // changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue + // a warning that it's going to be a frustrating journey. + Current.Value = allBeatmapsItem; + Schedule(() => + { + // current may have changed before the scheduled call is run. + if (Current.Value != allBeatmapsItem) + return; + + Current.Value = filters.SingleOrDefault(f => f.Collection?.ID == selectedItem.Collection?.ID) ?? filters[0]; + }); + + // Trigger an external re-filter if the current item was in the change set. + RequestFilter?.Invoke(); + break; + } + } + } + } + + private Live? lastFiltered; + + private void selectionChanged(ValueChangedEvent filter) + { + // May be null during .Clear(). + if (filter.NewValue.IsNull()) + return; + + // Never select the manage collection filter - rollback to the previous filter. + // This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value. + if (filter.NewValue is ManageCollectionsFilterMenuItem) + { + Current.Value = filter.OldValue; + manageCollectionsDialog?.Show(); + return; + } + + var newCollection = filter.NewValue.Collection; + + // This dropdown be weird. + // We only care about filtering if the actual collection has changed. + if (newCollection != lastFiltered) + { + RequestFilter?.Invoke(); + lastFiltered = newCollection; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + realmSubscription?.Dispose(); + } + + protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName; + + protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu(); + + protected virtual ShearedCollectionDropdownMenu CreateCollectionMenu() => new ShearedCollectionDropdownMenu(); + + protected partial class ShearedCollectionDropdownMenu : ShearedDropdownMenu + { + public ShearedCollectionDropdownMenu() + { + MaxHeight = 200; + } + + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableCollectionMenuItem(item) + { + BackgroundColourHover = HoverColour, + BackgroundColourSelected = SelectionColour + }; + } + + protected partial class DrawableCollectionMenuItem : ShearedDropdownMenu.ShearedMenuItem + { + private IconButton addOrRemoveButton = null!; + + private bool beatmapInCollection; + + private readonly Live? collection; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + public DrawableCollectionMenuItem(MenuItem item) + : base(item) + { + collection = ((DropdownMenuItem)item).Value.Collection; + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(addOrRemoveButton = new NoFocusChangeIconButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Shear = -OsuGame.SHEAR, + X = -OsuScrollContainer.SCROLL_BAR_WIDTH, + Scale = new Vector2(0.65f), + Action = addOrRemove, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (collection != null) + { + beatmap.BindValueChanged(_ => + { + beatmapInCollection = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.Value.BeatmapInfo.MD5Hash)); + + addOrRemoveButton.Enabled.Value = !beatmap.IsDefault; + addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare; + addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap"; + + updateButtonVisibility(); + }, true); + } + + updateButtonVisibility(); + } + + protected override bool OnHover(HoverEvent e) + { + updateButtonVisibility(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateButtonVisibility(); + base.OnHoverLost(e); + } + + protected override void OnSelectChange() + { + base.OnSelectChange(); + updateButtonVisibility(); + } + + private void updateButtonVisibility() + { + if (collection == null) + addOrRemoveButton.Alpha = 0; + else + addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0; + } + + private void addOrRemove() + { + Debug.Assert(collection != null); + + collection.PerformWrite(c => + { + if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash)) + c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash); + }); + } + + protected override Drawable CreateContent() => (Content)base.CreateContent(); + + private partial class NoFocusChangeIconButton : IconButton + { + public override bool ChangeFocusOnClick => false; + } + } + } +} From 2c690ae94c334925d0cfdad1415d00bdcb06452c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 07:38:11 +0200 Subject: [PATCH 258/281] Fix code quality --- osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 3 ++- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 23cd262dd0..5703ee754c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.PolygonExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("toggle expanded", () => { - if (leaderboard != null) + if (leaderboard.IsNotNull()) leaderboard.Expanded.Value = !leaderboard.Expanded.Value; }); diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 85f5281bef..92baa46695 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Skinning; using osuTK; using osuTK.Graphics; From b02ff0f95c166d0ee0fcdc4b49c9899ad064f07f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 16:24:31 +0900 Subject: [PATCH 259/281] Update framework --- osu.Android.props | 2 +- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8e383a705c..98ad145482 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 2fa83c3ab0..6949aea22e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From e44b134552275c1023c95585f925ee66e3b5fdf9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 17:30:31 +0900 Subject: [PATCH 260/281] Move number formatting to extension method and reuse for `AudioOffsetAdjustmentControl` Also adds some basic test coverage. --- .../NumberFormattingExtensionsTest.cs | 46 +++++++++++++++++ .../Extensions/NumberFormattingExtensions.cs | 51 +++++++++++++++++++ .../Graphics/UserInterface/OsuSliderBar.cs | 35 +------------ .../Audio/AudioOffsetAdjustControl.cs | 4 +- 4 files changed, 101 insertions(+), 35 deletions(-) create mode 100644 osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs create mode 100644 osu.Game/Extensions/NumberFormattingExtensions.cs diff --git a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs new file mode 100644 index 0000000000..7dcbc6f24a --- /dev/null +++ b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Extensions; + +namespace osu.Game.Tests.Extensions +{ + [TestFixture] + public class NumberFormattingExtensionsTest + { + [TestCase(-1, false, 0, ExpectedResult = "-1")] + [TestCase(0, false, 0, ExpectedResult = "0")] + [TestCase(1, false, 0, ExpectedResult = "1")] + [TestCase(500, false, 10, ExpectedResult = "500")] + [TestCase(-1, true, 0, ExpectedResult = "-1%")] + [TestCase(0, true, 0, ExpectedResult = "0%")] + [TestCase(1, true, 0, ExpectedResult = "1%")] + [TestCase(50, true, 0, ExpectedResult = "50%")] + public string TestInteger(int input, bool percent, int decimalDigits) + { + return input.ToStandardFormattedString(decimalDigits, percent); + } + + [TestCase(-1, false, 0, ExpectedResult = "-1")] + [TestCase(-1e-6, false, 0, ExpectedResult = "0")] + [TestCase(-1e-6, false, 6, ExpectedResult = "-0.000001")] + [TestCase(0, false, 10, ExpectedResult = "0")] + [TestCase(0, false, 0, ExpectedResult = "0")] + [TestCase(1e-6, false, 0, ExpectedResult = "0")] + [TestCase(1e-6, false, 6, ExpectedResult = "0.000001")] + [TestCase(1, false, 0, ExpectedResult = "1")] + [TestCase(1.528, false, 2, ExpectedResult = "1.53")] + [TestCase(500, false, 10, ExpectedResult = "500")] + [TestCase(-0.1, true, 0, ExpectedResult = "-10%")] + [TestCase(0, true, 0, ExpectedResult = "0%")] + [TestCase(0.4, true, 0, ExpectedResult = "40%")] + [TestCase(0.48333, true, 2, ExpectedResult = "48%")] + [TestCase(0.48333, true, 4, ExpectedResult = "48.33%")] + [TestCase(1, true, 0, ExpectedResult = "100%")] + public string TestDouble(double input, bool percent, int decimalDigits) + { + return input.ToStandardFormattedString(decimalDigits, percent); + } + } +} diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs new file mode 100644 index 0000000000..5832e4ba9b --- /dev/null +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using System.Numerics; +using osu.Game.Utils; + +namespace osu.Game.Extensions +{ + public static class NumberFormattingExtensions + { + /// + /// For a given numeric type, return a formatted string in the standard format we use for display everywhere. + /// + /// The numeric value. + /// The maximum number of decimals to be considered in the original value. + /// Whether the output should be a percentage. For integer types, 0-100 is mapped to 0-100%; for other types 0-1 is mapped to 0-100%. + /// The formatted output. + public static string ToStandardFormattedString(this T value, int maxDecimalDigits, bool asPercentage) where T : struct, INumber, IMinMaxValue + { + double floatValue = double.CreateTruncating(value); + + decimal decimalPrecision = normalise(decimal.CreateTruncating(value), maxDecimalDigits); + + // Find the number of significant digits (we could have less than maxDecimalDigits after normalize()) + int significantDigits = FormatUtils.FindPrecision(decimalPrecision); + + if (asPercentage) + { + if (value is int) + floatValue /= 100; + + return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}"); + } + + string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; + + return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"; + } + + /// + /// Removes all non-significant digits, keeping at most a requested number of decimal digits. + /// + /// The decimal to normalize. + /// The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value. + /// The normalised decimal. + private static decimal normalise(decimal d, int sd) + => decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 4b52ac4a3a..24b0e7b0f5 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -1,9 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Numerics; -using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -11,7 +9,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Framework.Utils; -using osu.Game.Utils; +using osu.Game.Extensions; namespace osu.Game.Graphics.UserInterface { @@ -85,35 +83,6 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); } - public LocalisableString GetDisplayableValue(T value) - { - if (CurrentNumber.IsInteger) - return int.CreateTruncating(value).ToString("N0"); - - double floatValue = double.CreateTruncating(value); - - decimal decimalPrecision = normalise(decimal.CreateTruncating(CurrentNumber.Precision), max_decimal_digits); - - // Find the number of significant digits (we could have less than 5 after normalize()) - int significantDigits = FormatUtils.FindPrecision(decimalPrecision); - - if (DisplayAsPercentage) - { - return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}"); - } - - string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; - - return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"; - } - - /// - /// Removes all non-significant digits, keeping at most a requested number of decimal digits. - /// - /// The decimal to normalize. - /// The maximum number of decimal digits to keep. The final result may have fewer decimal digits than this value. - /// The normalised decimal. - private decimal normalise(decimal d, int sd) - => decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); + public LocalisableString GetDisplayableValue(T value) => CurrentNumber.Value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage); } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs index 04496428ee..2629cd2183 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioOffsetAdjustControl.cs @@ -8,13 +8,13 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -169,7 +169,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio else { applySuggestion.Enabled.Value = true; - hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0")); + hintText.Text = AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.Value.ToStandardFormattedString(0, false)); } } From 62d83cff9a31dcb26b311788c628921e4a0b82b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 10:55:43 +0200 Subject: [PATCH 261/281] Add a test for negative zero (yes, *negative zero*) --- osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs index 7dcbc6f24a..fca39f86ec 100644 --- a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs +++ b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Extensions [TestCase(-1e-6, false, 6, ExpectedResult = "-0.000001")] [TestCase(0, false, 10, ExpectedResult = "0")] [TestCase(0, false, 0, ExpectedResult = "0")] + [TestCase(double.NegativeZero, false, 0, ExpectedResult = "0")] [TestCase(1e-6, false, 0, ExpectedResult = "0")] [TestCase(1e-6, false, 6, ExpectedResult = "0.000001")] [TestCase(1, false, 0, ExpectedResult = "1")] From c28f2a932c0cc108b654da9c303a6cbb5b3d9918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 11:23:20 +0200 Subject: [PATCH 262/281] Add failing test --- .../Extensions/NumberFormattingExtensionsTest.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs index fca39f86ec..b02bf01019 100644 --- a/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs +++ b/osu.Game.Tests/Extensions/NumberFormattingExtensionsTest.cs @@ -43,5 +43,12 @@ namespace osu.Game.Tests.Extensions { return input.ToStandardFormattedString(decimalDigits, percent); } + + [Test] + [SetCulture("fr-FR")] + public void TestCultureInsensitivity() + { + Assert.That(0.4.ToStandardFormattedString(maxDecimalDigits: 2, asPercentage: true), Is.EqualTo("40%")); + } } } From e5636a84f1f3cdebb32290c31841bb073f3fdbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 11:23:26 +0200 Subject: [PATCH 263/281] Fix culture variance in new formatting helper --- osu.Game/Extensions/NumberFormattingExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Extensions/NumberFormattingExtensions.cs b/osu.Game/Extensions/NumberFormattingExtensions.cs index 5832e4ba9b..618b086a5b 100644 --- a/osu.Game/Extensions/NumberFormattingExtensions.cs +++ b/osu.Game/Extensions/NumberFormattingExtensions.cs @@ -31,12 +31,12 @@ namespace osu.Game.Extensions if (value is int) floatValue /= 100; - return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}"); + return floatValue.ToString($@"0.{new string('0', Math.Max(0, significantDigits - 2))}%", CultureInfo.InvariantCulture); } string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty; - return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"; + return FormattableString.Invariant($"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}"); } /// From 39f9eabf40b39126fbf62ad3664cca0357f92c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:01:04 +0200 Subject: [PATCH 264/281] Add failing test for incorrect score position treatment --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 5703ee754c..e8b5326244 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -205,6 +205,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("tracked player has null position", () => leaderboard.TrackedScore?.ScorePosition, () => Is.Null); else AddUntilStep("tracked player is #50", () => leaderboard.TrackedScore?.ScorePosition, () => Is.EqualTo(50)); + + AddStep("move tracked player to top", () => leaderboard.TrackedScore!.TotalScore.Value = 8_000_000); + AddUntilStep("all players have non-null position", () => leaderboard.AllScores.Select(s => s.ScorePosition), () => Does.Not.Contain(null)); } private void addLocalPlayer() @@ -252,6 +255,8 @@ namespace osu.Game.Tests.Visual.Gameplay public IEnumerable GetAllScoresForUsername(string username) => Flow.Where(i => i.User?.Username == username); + + public IEnumerable AllScores => Flow; } private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider From 3ecf56b6f60538af9f31b12a8ae6c0212430268d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:02:02 +0200 Subject: [PATCH 265/281] Fix incorrect score position treatment if last score on partial leaderboard isn't tracked --- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 92baa46695..7cfdb9631b 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -180,8 +180,9 @@ namespace osu.Game.Screens.Play.HUD for (int i = 0; i < Flow.Count; i++) { - Flow.SetLayoutPosition(orderedByScore[i], i); - orderedByScore[i].ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true ? null : i + 1; + var score = orderedByScore[i]; + Flow.SetLayoutPosition(score, i); + score.ScorePosition = i + 1 == Flow.Count && leaderboardProvider?.IsPartial == true && score.Tracked ? null : i + 1; } sorting.Validate(); From 006670c4423d96b522b5ed0c297f1818047a1c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:02:21 +0200 Subject: [PATCH 266/281] Add clarification to `IsPartial` xmldoc --- .../Select/Leaderboards/IGameplayLeaderboardProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs index 0138f855e2..0d88e7bf6c 100644 --- a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -20,6 +20,9 @@ namespace osu.Game.Screens.Select.Leaderboards /// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores), /// or is a full leaderboard (contains all scores that there will ever be). /// + /// + /// If this is and a tracked score is last on the leaderboard, it will show an "unknown" score position. + /// bool IsPartial { get; } } } From b80ea2647542995fda6d0c1159db42d79757b0e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Apr 2025 19:07:44 +0900 Subject: [PATCH 267/281] Add accounting of nested group items for group panel display purposes --- osu.Game/Graphics/Carousel/CarouselItem.cs | 5 +++++ .../SelectV2/BeatmapCarouselFilterGrouping.cs | 12 +++++++++++- osu.Game/Screens/SelectV2/PanelGroup.cs | 6 +++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Carousel/CarouselItem.cs b/osu.Game/Graphics/Carousel/CarouselItem.cs index 223c8d9869..4904b9f13d 100644 --- a/osu.Game/Graphics/Carousel/CarouselItem.cs +++ b/osu.Game/Graphics/Carousel/CarouselItem.cs @@ -44,6 +44,11 @@ namespace osu.Game.Graphics.Carousel /// public bool IsExpanded { get; set; } + /// + /// The number of nested items underneath this header. Should only be used for headers of groups. + /// + public int NestedItemCount { get; set; } + public CarouselItem(object model) { Model = model; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 3360437544..a628595477 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -47,7 +47,9 @@ namespace osu.Game.Screens.SelectV2 var newItems = new List(); BeatmapInfo? lastBeatmap = null; + GroupDefinition? lastGroup = null; + CarouselItem? lastGroupItem = null; HashSet? currentGroupItems = null; HashSet? currentSetItems = null; @@ -69,7 +71,7 @@ namespace osu.Game.Screens.SelectV2 groupItems[newGroup] = currentGroupItems = new HashSet(); lastGroup = newGroup; - addItem(new CarouselItem(newGroup) + addItem(lastGroupItem = new CarouselItem(newGroup) { DrawHeight = PanelGroup.HEIGHT, DepthLayer = -2, @@ -84,6 +86,9 @@ namespace osu.Game.Screens.SelectV2 { setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + if (lastGroupItem != null) + lastGroupItem.NestedItemCount++; + addItem(new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = PanelBeatmapSet.HEIGHT, @@ -91,6 +96,11 @@ namespace osu.Game.Screens.SelectV2 }); } } + else + { + if (lastGroupItem != null) + lastGroupItem.NestedItemCount++; + } addItem(item); lastBeatmap = beatmap; diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs index ac4857d2f3..4370146dbc 100644 --- a/osu.Game/Screens/SelectV2/PanelGroup.cs +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -26,6 +26,7 @@ namespace osu.Game.Screens.SelectV2 private Drawable iconContainer = null!; private OsuSpriteText titleText = null!; private TrianglesV2 triangles = null!; + private OsuSpriteText countText = null!; private Box glow = null!; [Resolved] @@ -99,13 +100,11 @@ namespace osu.Game.Screens.SelectV2 RelativeSizeAxes = Axes.Both, Colour = Color4.Black.Opacity(0.7f), }, - new OsuSpriteText + countText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), - // TODO: requires Carousel/CarouselItem-side implementation - Text = "43", UseFullGlyphHeight = false, } }, @@ -144,6 +143,7 @@ namespace osu.Game.Screens.SelectV2 GroupDefinition group = (GroupDefinition)Item.Model; titleText.Text = group.Title; + countText.Text = Item.NestedItemCount.ToString("N0"); } } } From 6d258d4ed5182341b66fe1e425dbebc1a81d0567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 12:11:42 +0200 Subject: [PATCH 268/281] Remove unnecessary interface --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 30 +-------- .../Play/HUD/DrawableGameplayLeaderboard.cs | 6 +- .../HUD/DrawableGameplayLeaderboardScore.cs | 3 +- .../Play/HUD/IGameplayLeaderboardScore.cs | 67 ------------------- .../Leaderboards/GameplayLeaderboardScore.cs | 58 +++++++++++++++- .../IGameplayLeaderboardProvider.cs | 3 +- .../MultiplayerLeaderboardProvider.cs | 5 +- .../SoloGameplayLeaderboardProvider.cs | 5 +- 8 files changed, 69 insertions(+), 108 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index e8b5326244..bef43b3108 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.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; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -16,10 +15,8 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select.Leaderboards; -using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -238,7 +235,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void createLeaderboardScore(BindableLong score, APIUser user, bool isTracked = false) { - var leaderboardScore = new TestDrawableGameplayLeaderboardScore(user, isTracked, score); + var leaderboardScore = new GameplayLeaderboardScore(user, isTracked, score); leaderboardProvider.Scores.Add(leaderboardScore); } @@ -261,30 +258,9 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestGameplayLeaderboardProvider : IGameplayLeaderboardProvider { - IBindableList IGameplayLeaderboardProvider.Scores => Scores; - public BindableList Scores { get; } = new BindableList(); + IBindableList IGameplayLeaderboardProvider.Scores => Scores; + public BindableList Scores { get; } = new BindableList(); public bool IsPartial { get; set; } } - - private class TestDrawableGameplayLeaderboardScore : IGameplayLeaderboardScore - { - public IUser User { get; } - public bool Tracked { get; } - public BindableLong TotalScore { get; } = new BindableLong(); - public BindableDouble Accuracy { get; } = new BindableDouble(); - public BindableInt Combo { get; } = new BindableInt(); - public BindableBool HasQuit { get; } = new BindableBool(); - public Bindable DisplayOrder { get; } = new BindableLong(); - public Func GetDisplayScore { get; set; } - public Colour4? TeamColour => null; - - public TestDrawableGameplayLeaderboardScore(IUser user, bool isTracked, Bindable totalScore) - { - User = user; - Tracked = isTracked; - TotalScore.BindTo(totalScore); - GetDisplayScore = _ => TotalScore.Value; - } - } } } diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 7cfdb9631b..f60d12d84f 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } - private readonly IBindableList scores = new BindableList(); + private readonly IBindableList scores = new BindableList(); private const int max_panels = 8; @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Adds a player to the leaderboard. /// - public void Add(IGameplayLeaderboardScore score) + public void Add(GameplayLeaderboardScore score) { var drawable = CreateLeaderboardScoreDrawable(score); @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Play.HUD scroll.ScrollToStart(false); } - protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(IGameplayLeaderboardScore score) => + protected virtual DrawableGameplayLeaderboardScore CreateLeaderboardScoreDrawable(GameplayLeaderboardScore score) => new DrawableGameplayLeaderboardScore(score); protected override void Update() diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index f04d3ee492..b14e31983c 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; using osu.Game.Users.Drawables; using osu.Game.Utils; @@ -114,7 +115,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Creates a new . /// - public DrawableGameplayLeaderboardScore(IGameplayLeaderboardScore score) + public DrawableGameplayLeaderboardScore(GameplayLeaderboardScore score) { User = score.User; Tracked = score.Tracked; diff --git a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs deleted file mode 100644 index 20c7b16d79..0000000000 --- a/osu.Game/Screens/Play/HUD/IGameplayLeaderboardScore.cs +++ /dev/null @@ -1,67 +0,0 @@ -// 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.Bindables; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Scoring; -using osu.Game.Users; - -namespace osu.Game.Screens.Play.HUD -{ - /// - /// Represents a score shown on a gameplay leaderboard. - /// The score is expected to update itself as gameplay progresses. - /// - public interface IGameplayLeaderboardScore - { - /// - /// The user playing. - /// - IUser User { get; } - - /// - /// Whether the score is being tracked. - /// Generally understood as true when this score is the score of the local user currently playing. - /// - bool Tracked { get; } - - /// - /// The current total of the score. - /// - BindableLong TotalScore { get; } - - /// - /// The current accuracy of the score. - /// - BindableDouble Accuracy { get; } - - /// - /// The current combo of the score. - /// - BindableInt Combo { get; } - - /// - /// Whether the user playing has quit. - /// - BindableBool HasQuit { get; } - - /// - /// An optional value to guarantee stable ordering. - /// Lower numbers will appear higher in cases of ties. - /// - Bindable DisplayOrder { get; } - - /// - /// A custom function which handles converting a score to a display score using a provide . - /// - /// - /// If no function is provided, will be used verbatim. - Func GetDisplayScore { get; set; } - - /// - /// The colour of the team that the user playing is on, if any. - /// - Colour4? TeamColour { get; } - } -} diff --git a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs index ba3e4f728b..2655fd8dba 100644 --- a/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/GameplayLeaderboardScore.cs @@ -8,21 +8,64 @@ using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; -using osu.Game.Screens.Play.HUD; using osu.Game.Users; namespace osu.Game.Screens.Select.Leaderboards { - public class GameplayLeaderboardScore : IGameplayLeaderboardScore + /// + /// Represents a score shown on a gameplay leaderboard. + /// The score is expected to update itself as gameplay progresses. + /// + public class GameplayLeaderboardScore { + /// + /// The user playing. + /// public IUser User { get; } + + /// + /// Whether the score is being tracked. + /// Generally understood as true when this score is the score of the local user currently playing. + /// public bool Tracked { get; } + + /// + /// The current total of the score. + /// public BindableLong TotalScore { get; } = new BindableLong(); + + /// + /// The current accuracy of the score. + /// public BindableDouble Accuracy { get; } = new BindableDouble(); + + /// + /// The current combo of the score. + /// public BindableInt Combo { get; } = new BindableInt(); + + /// + /// Whether the user playing has quit. + /// public BindableBool HasQuit { get; } = new BindableBool(); + + /// + /// An optional value to guarantee stable ordering. + /// Lower numbers will appear higher in cases of ties. + /// public Bindable DisplayOrder { get; } = new BindableLong(); + + /// + /// A custom function which handles converting a score to a display score using a provided . + /// + /// + /// If no function is provided, will be used verbatim. + /// public Func GetDisplayScore { get; set; } + + /// + /// The colour of the team that the user playing is on, if any. + /// public Colour4? TeamColour { get; init; } public GameplayLeaderboardScore(IUser user, ScoreProcessor scoreProcessor, bool tracked) @@ -55,5 +98,16 @@ namespace osu.Game.Screens.Select.Leaderboards DisplayOrder.Value = scoreInfo.OnlineID > 0 ? scoreInfo.OnlineID : scoreInfo.Date.ToUnixTimeSeconds(); GetDisplayScore = scoreInfo.GetDisplayScore; } + + /// + /// Used for testing. + /// + internal GameplayLeaderboardScore(IUser user, bool tracked, Bindable displayScore) + { + User = user; + Tracked = tracked; + TotalScore.BindTarget = displayScore; + GetDisplayScore = _ => displayScore.Value; + } } } diff --git a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs index 0d88e7bf6c..4399c422b4 100644 --- a/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/IGameplayLeaderboardProvider.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Screens.Select.Leaderboards { @@ -14,7 +13,7 @@ namespace osu.Game.Screens.Select.Leaderboards /// /// List of all scores to display on the leaderboard. /// - public IBindableList Scores { get; } + public IBindableList Scores { get; } /// /// Whether this leaderboard is a partial leaderboard (e.g. contains only the top 50 of all scores), diff --git a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs index 1c2b400164..edfccd0e7e 100644 --- a/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/MultiplayerLeaderboardProvider.cs @@ -21,7 +21,6 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; using osuTK.Graphics; namespace osu.Game.Screens.Select.Leaderboards @@ -29,8 +28,8 @@ namespace osu.Game.Screens.Select.Leaderboards [LongRunningLoad] public partial class MultiplayerLeaderboardProvider : CompositeComponent, IGameplayLeaderboardProvider { - public IBindableList Scores => scores; - private readonly BindableList scores = new BindableList(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); protected readonly Dictionary UserScores = new Dictionary(); public readonly SortedDictionary TeamScores = new SortedDictionary(); diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index 216fda8d9f..ac94d307c6 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Screens.Select.Leaderboards { @@ -15,8 +14,8 @@ namespace osu.Game.Screens.Select.Leaderboards { public bool IsPartial { get; private set; } - public IBindableList Scores => scores; - private readonly BindableList scores = new BindableList(); + public IBindableList Scores => scores; + private readonly BindableList scores = new BindableList(); [BackgroundDependencyLoader] private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) From f480765bf44b0ea797f52cc9d52cfaa56a3c50d8 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 16 Apr 2025 06:53:46 -0400 Subject: [PATCH 269/281] Add drawable wrapper for shear alignment purposes --- .../TestSceneShearAligningWrapper.cs | 132 ++++++++++++++++++ .../Containers/ShearAligningWrapper.cs | 49 +++++++ 2 files changed, 181 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs create mode 100644 osu.Game/Graphics/Containers/ShearAligningWrapper.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs new file mode 100644 index 0000000000..eb65de8fdc --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearAligningWrapper.cs @@ -0,0 +1,132 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneShearAligningWrapper : OsuTestScene + { + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + private ShearedBox first = null!; + private ShearedBox second = null!; + private ShearedBox third = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 200f, + AutoSizeAxes = Axes.Y, + Shear = OsuGame.SHEAR, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + new ShearAligningWrapper(first = new ShearedBox("Text 1", OsuColour.Gray(0.4f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + new ShearAligningWrapper(second = new ShearedBox("Text 2", OsuColour.Gray(0.3f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + new ShearAligningWrapper(third = new ShearedBox("Text 3", OsuColour.Gray(0.2f)) + { + RelativeSizeAxes = Axes.X, + Height = 30, + }), + } + } + }, + }; + }); + + [SetUpSteps] + public void SetUpSteps() + { + AddSliderStep("box 1 height", 0, 100, 30, h => + { + if (first.IsNotNull()) + first.Height = h; + }); + AddSliderStep("box 2 height", 0, 100, 30, h => + { + if (second.IsNotNull()) + second.Height = h; + }); + AddSliderStep("box 3 height", 0, 100, 30, h => + { + if (third.IsNotNull()) + third.Height = h; + }); + } + + public partial class ShearedBox : Container + { + private readonly string text; + private readonly Color4 boxColour; + + public ShearedBox(string text, Color4 boxColour) + { + this.text = text; + this.boxColour = boxColour; + } + + [BackgroundDependencyLoader] + private void load() + { + CornerRadius = 10; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = boxColour, + }, + new OsuSpriteText + { + Text = text, + Colour = Color4.White, + Shear = -OsuGame.SHEAR, + Font = OsuFont.Torus.With(size: 24), + Margin = new MarginPadding { Left = 50 }, + } + }; + } + } + } +} diff --git a/osu.Game/Graphics/Containers/ShearAligningWrapper.cs b/osu.Game/Graphics/Containers/ShearAligningWrapper.cs new file mode 100644 index 0000000000..d720120b4f --- /dev/null +++ b/osu.Game/Graphics/Containers/ShearAligningWrapper.cs @@ -0,0 +1,49 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; +using osuTK; + +namespace osu.Game.Graphics.Containers +{ + /// + /// Adds left padding based on direct parent to make sheared pieces in a vertical flow aligned appropriately. + /// + /// + /// See associated test scene for further demonstration. + /// + public partial class ShearAligningWrapper : CompositeDrawable + { + private readonly LayoutValue layout = new LayoutValue(Invalidation.MiscGeometry); + + public ShearAligningWrapper(Drawable drawable) + { + RelativeSizeAxes = drawable.RelativeSizeAxes; + AutoSizeAxes = Axes.Both & ~drawable.RelativeSizeAxes; + + InternalChild = drawable; + + AddLayout(layout); + } + + protected override void Update() + { + base.Update(); + + if (!layout.IsValid) + { + updateLayout(); + layout.Validate(); + } + } + + private void updateLayout() + { + float shearWidth = OsuGame.SHEAR.X * Parent!.DrawHeight; + float relativeY = Parent!.DrawHeight == 0 ? 0 : InternalChild.ToSpaceOfOtherDrawable(Vector2.Zero, Parent).Y / Parent!.DrawHeight; + Padding = new MarginPadding { Left = shearWidth * relativeY }; + } + } +} From 5791375b38bb16838e897f8935c4564661425cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:03:32 +0200 Subject: [PATCH 270/281] Fix rate adjust no longer showing the rate if custom "Accidentally" removed in 6e635f124aee13d3d95d26ba10a08c321360ceb7 apparently. --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 358034541c..a824731830 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -34,5 +34,7 @@ namespace osu.Game.Rulesets.Mods yield return ("Speed change", $"{SpeedChange.Value:N2}x"); } } + + public override string ExtendedIconInformation => SpeedChange.IsDefault ? string.Empty : FormattableString.Invariant($"{SpeedChange.Value:N2}x"); } } From 20b2cc8251b7ad468a7e9d9b00b494822bd54b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:21:43 +0200 Subject: [PATCH 271/281] Add failing test coverage --- osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 5da60966b2..4b90bec784 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -48,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestVideoSize() + public void TestVideo() { AddStep("load storyboard with only video", () => { @@ -56,6 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false); }); + AddAssert("storyboard video present in hierarchy", () => this.ChildrenOfType().Any()); AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f)); } From 2761ee005dafb9f2f5eea1c5a958e2c1cdb64bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 14:23:43 +0200 Subject: [PATCH 272/281] Fix storyboard videos not displaying Regressed with 102085668f84bd80f1717f101adc22fc7075e7fa because the stupid magic alpha transform addition was also implicitly changing the value of `IsDrawable` from false to true because that property checks for presence of any commands. Apparently past me, in his infinite wisdom, did not decide it pertinent to test that change against, you know, *a beatmap with a storyboard*. Great job, past me, good show all around. --- osu.Game/Storyboards/StoryboardSprite.cs | 2 +- osu.Game/Storyboards/StoryboardVideo.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index e10edfefe1..5b3e7c3919 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -17,7 +17,7 @@ namespace osu.Game.Storyboards private readonly List triggerGroups = new List(); public string Path { get; } - public bool IsDrawable => HasCommands; + public virtual bool IsDrawable => HasCommands; public Anchor Origin; public Vector2 InitialPosition; diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index fb4ac56e98..5a9eb533c6 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -19,6 +19,8 @@ namespace osu.Game.Storyboards public override double StartTime { get; } + public override bool IsDrawable => true; + public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this); } } From c29f59fcdb964288ec988eb7c41e74771848bdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Apr 2025 20:02:34 +0200 Subject: [PATCH 273/281] Fix gameplay leaderboard showing scores from wrong beatmaps Kind of a big oversight this. In wanting to get the leaderboard refactors to move forward I sort of didn't realise the fact that all of the error handling related to online status and such in `BeatmapLeaderboard` kind of... can't stay there... It's also an all-or-nothing business too - moving this stuff can't really be done only in part. Not sure whether tests are warranted if it's more or less moving logic across? --- .../Online/Leaderboards/LeaderboardManager.cs | 55 +++++++++++++++++-- .../Online/Leaderboards/LeaderboardState.cs | 15 ++--- .../Select/Leaderboards/BeatmapLeaderboard.cs | 49 ++--------------- 3 files changed, 63 insertions(+), 56 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index ff3fe39a96..6629781d2c 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -54,6 +54,9 @@ namespace osu.Game.Online.Leaderboards lastFetchCompletionSource?.TrySetCanceled(); scores.Value = null; + if (newCriteria.Beatmap == null || newCriteria.Ruleset == null) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoneSelected)); + switch (newCriteria.Scope) { case BeatmapLeaderboardScope.Local: @@ -72,6 +75,21 @@ namespace osu.Game.Online.Leaderboards default: { + if (!api.IsLoggedIn) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotLoggedIn)); + + if (!newCriteria.Ruleset.IsLegacyRuleset()) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.RulesetUnavailable)); + + if (newCriteria.Beatmap.OnlineID <= 0 || newCriteria.Beatmap.Status <= BeatmapOnlineStatus.Pending) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.BeatmapUnavailable)); + + if ((newCriteria.Scope.RequiresSupporter(newCriteria.ExactMods != null)) && !api.LocalUser.Value.IsSupporter) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NotSupporter)); + + if (newCriteria.Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) + return Task.FromResult(scores.Value = LeaderboardScores.Failure(LeaderboardFailState.NoTeam)); + var onlineFetchCompletionSource = new TaskCompletionSource(); lastFetchCompletionSource = onlineFetchCompletionSource; @@ -92,7 +110,7 @@ namespace osu.Game.Online.Leaderboards if (inFlightOnlineRequest != null && !newRequest.Equals(inFlightOnlineRequest)) return; - var result = new LeaderboardScores + var result = LeaderboardScores.Success ( response.Scores.Select(s => s.ToScoreInfo(rulesets, newCriteria.Beatmap)).OrderByTotalScore(), response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) @@ -101,7 +119,7 @@ namespace osu.Game.Online.Leaderboards if (onlineFetchCompletionSource.TrySetResult(result)) scores.Value = result; }; - newRequest.Failure += ex => onlineFetchCompletionSource.TrySetException(ex); + newRequest.Failure += _ => onlineFetchCompletionSource.TrySetResult(LeaderboardScores.Failure(LeaderboardFailState.NetworkFailure)); api.Queue(inFlightOnlineRequest = newRequest); return onlineFetchCompletionSource.Task; } @@ -138,7 +156,7 @@ namespace osu.Game.Online.Leaderboards newScores = newScores.Detach().OrderByTotalScore(); - scores.Value = new LeaderboardScores(newScores, null); + scores.Value = LeaderboardScores.Success(newScores, null); if (localFetchCompletionSource != null && localFetchCompletionSource == lastFetchCompletionSource) { @@ -149,14 +167,18 @@ namespace osu.Game.Online.Leaderboards } public record LeaderboardCriteria( - BeatmapInfo Beatmap, - RulesetInfo Ruleset, + BeatmapInfo? Beatmap, + RulesetInfo? Ruleset, BeatmapLeaderboardScope Scope, Mod[]? ExactMods ); - public record LeaderboardScores(IEnumerable TopScores, ScoreInfo? UserScore) + public record LeaderboardScores { + public IEnumerable TopScores { get; } + public ScoreInfo? UserScore { get; } + public LeaderboardFailState? FailState { get; } + public IEnumerable AllScores { get @@ -168,5 +190,26 @@ namespace osu.Game.Online.Leaderboards yield return UserScore; } } + + private LeaderboardScores(IEnumerable topScores, ScoreInfo? userScore, LeaderboardFailState? failState) + { + TopScores = topScores; + UserScore = userScore; + FailState = failState; + } + + public static LeaderboardScores Success(IEnumerable topScores, ScoreInfo? userScore) => new LeaderboardScores(topScores, userScore, null); + public static LeaderboardScores Failure(LeaderboardFailState failState) => new LeaderboardScores([], null, failState); + } + + public enum LeaderboardFailState + { + NetworkFailure = -1, + BeatmapUnavailable = -2, + RulesetUnavailable = -3, + NoneSelected = -4, + NotLoggedIn = -5, + NotSupporter = -6, + NoTeam = -7 } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs index dbd982acf2..b0b45ef04e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardState.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs @@ -7,13 +7,14 @@ namespace osu.Game.Online.Leaderboards { Success, Retrieving, - NetworkFailure, - BeatmapUnavailable, - RulesetUnavailable, - NoneSelected, NoScores, - NotLoggedIn, - NotSupporter, - NoTeam + + NetworkFailure = LeaderboardFailState.NetworkFailure, + BeatmapUnavailable = LeaderboardFailState.BeatmapUnavailable, + RulesetUnavailable = LeaderboardFailState.RulesetUnavailable, + NoneSelected = LeaderboardFailState.NoneSelected, + NotLoggedIn = LeaderboardFailState.NotLoggedIn, + NotSupporter = LeaderboardFailState.NotSupporter, + NoTeam = LeaderboardFailState.NoTeam, } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2896e7eab4..f5fefa52b5 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -8,7 +8,6 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; @@ -71,9 +70,6 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private IBindable> mods { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - [Resolved] private LeaderboardManager leaderboardManager { get; set; } = null!; @@ -94,44 +90,7 @@ namespace osu.Game.Screens.Select.Leaderboards protected override APIRequest? FetchScores(CancellationToken cancellationToken) { var fetchBeatmapInfo = BeatmapInfo; - - if (fetchBeatmapInfo == null) - { - SetErrorState(LeaderboardState.NoneSelected); - return null; - } - - var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; - - if (!api.IsLoggedIn && IsOnlineScope) - { - SetErrorState(LeaderboardState.NotLoggedIn); - return null; - } - - if (!fetchRuleset.IsLegacyRuleset()) - { - SetErrorState(LeaderboardState.RulesetUnavailable); - return null; - } - - if ((fetchBeatmapInfo.OnlineID <= 0 || fetchBeatmapInfo.Status <= BeatmapOnlineStatus.Pending) && IsOnlineScope) - { - SetErrorState(LeaderboardState.BeatmapUnavailable); - return null; - } - - if (Scope.RequiresSupporter(filterMods) && !api.LocalUser.Value.IsSupporter) - { - SetErrorState(LeaderboardState.NotSupporter); - return null; - } - - if (Scope == BeatmapLeaderboardScope.Team && api.LocalUser.Value.Team == null) - { - SetErrorState(LeaderboardState.NoTeam); - return null; - } + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo?.Ruleset; leaderboardManager.FetchWithCriteriaAsync(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope, filterMods ? mods.Value.ToArray() : null)) .ContinueWith(t => @@ -145,8 +104,12 @@ namespace osu.Game.Screens.Select.Leaderboards fetchedScores.UnbindEvents(); fetchedScores.BindValueChanged(scores => { - if (scores.NewValue != null) + if (scores.NewValue == null) return; + + if (scores.NewValue.FailState == null) Schedule(() => SetScores(scores.NewValue.TopScores, scores.NewValue.UserScore)); + else + Schedule(() => SetErrorState((LeaderboardState)scores.NewValue.FailState)); }, true); }, cancellationToken); From d1f7afc8edbb4e88939b3b283dae1d6fb5e0f504 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sat, 19 Apr 2025 09:00:53 +0200 Subject: [PATCH 274/281] Change "Delete Difficulty" editor menu item type to destructive --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 572c4ce283..e238abbb25 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1266,7 +1266,7 @@ namespace osu.Game.Screens.Edit yield return createDifficultyCreationMenu(); yield return createDifficultySwitchMenu(); yield return new OsuMenuItemSpacer(); - yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }; + yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Destructive, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }; yield return new OsuMenuItemSpacer(); var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save)) { Hotkey = new Hotkey(PlatformAction.Save) }; From 99e882bfbc63b6dc17d65f6dde5738b9ccbe2263 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 20 Apr 2025 00:11:26 +0900 Subject: [PATCH 275/281] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 98ad145482..5bca6cc497 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 6949aea22e..d988adb6cf 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 267dccdd9afee0bad9742f5272684e1f10a36c2a Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sun, 20 Apr 2025 09:43:45 +0200 Subject: [PATCH 276/281] Fix slider tooltip text not updating with current value --- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 24b0e7b0f5..ca95d45042 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -83,6 +83,6 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); } - public LocalisableString GetDisplayableValue(T value) => CurrentNumber.Value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage); + public LocalisableString GetDisplayableValue(T value) => value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage); } } From d8df499e728cb827c4b90f75d7233d5ba75cd739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 08:49:27 +0200 Subject: [PATCH 277/281] Allow toggling leaderboard visibility in replays Closes https://github.com/ppy/osu/issues/31744 I guess. This isn't the resolution that I had in mind for this but my hand has been basically forced by user feedback to do this, at least in the short-term. --- .../Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs | 7 ------- osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs | 9 +-------- osu.Game/Screens/Play/ReplayPlayer.cs | 1 - osu.Game/Screens/Play/SoloPlayer.cs | 1 - 4 files changed, 1 insertion(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs index dbd14db818..6b2f5767f8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs @@ -57,7 +57,6 @@ namespace osu.Game.Tests.Visual.Gameplay Scores = { BindTarget = scores }, Anchor = Anchor.Centre, Origin = Anchor.Centre, - AlwaysVisible = { Value = false }, Expanded = { Value = true }, }; }); @@ -101,12 +100,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set config visible false", () => configVisibility.Value = false); AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0); - - AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true); - AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1); - - AddStep("set config visible true", () => configVisibility.Value = true); - AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1); } private static List createSampleScores() diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs index e9bb1d2101..b06c9b7be8 100644 --- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs @@ -30,12 +30,6 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private ScoreProcessor scoreProcessor { get; set; } = null!; - /// - /// Whether the leaderboard should be visible regardless of the configuration value. - /// This is true by default, but can be changed. - /// - public readonly Bindable AlwaysVisible = new Bindable(true); - public SoloGameplayLeaderboard(IUser trackingUser) { this.trackingUser = trackingUser; @@ -57,7 +51,6 @@ namespace osu.Game.Screens.Play.HUD // Alpha will be updated via `updateVisibility` below. Alpha = 0; - AlwaysVisible.BindValueChanged(_ => updateVisibility()); configVisibility.BindValueChanged(_ => updateVisibility(), true); } @@ -103,6 +96,6 @@ namespace osu.Game.Screens.Play.HUD } private void updateVisibility() => - this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration); + this.FadeTo(configVisibility.Value ? 1 : 0, duration); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a5952f3ff3..39f5d28e64 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -121,7 +121,6 @@ namespace osu.Game.Screens.Play protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { - AlwaysVisible = { Value = true }, Scores = { BindTarget = localScores } }; diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index ed5dea98cd..eae710bd1f 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -68,7 +68,6 @@ namespace osu.Game.Screens.Play protected override GameplayLeaderboard CreateGameplayLeaderboard() => new SoloGameplayLeaderboard(Score.ScoreInfo.User) { - AlwaysVisible = { Value = false }, Scores = { BindTarget = localScores } }; From 4d08c81e8d8c2597a198f8284a1f69c3189af525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 09:05:54 +0200 Subject: [PATCH 278/281] Move bindable list population to load complete to fix threading woes --- .../Leaderboards/SoloGameplayLeaderboardProvider.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs index ac94d307c6..5cbbb3f3b0 100644 --- a/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs +++ b/osu.Game/Screens/Select/Leaderboards/SoloGameplayLeaderboardProvider.cs @@ -17,9 +17,16 @@ namespace osu.Game.Screens.Select.Leaderboards public IBindableList Scores => scores; private readonly BindableList scores = new BindableList(); - [BackgroundDependencyLoader] - private void load(LeaderboardManager? leaderboardManager, GameplayState? gameplayState) + [Resolved] + private LeaderboardManager? leaderboardManager { get; set; } + + [Resolved] + private GameplayState? gameplayState { get; set; } + + protected override void LoadComplete() { + base.LoadComplete(); + var globalScores = leaderboardManager?.Scores.Value; IsPartial = leaderboardManager?.CurrentCriteria?.Scope != BeatmapLeaderboardScope.Local && globalScores?.TopScores.Count >= 50; From da1fc1013e07b8dafb0c409354f9d1cef971e449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 09:15:44 +0200 Subject: [PATCH 279/281] Bring back reading from config value --- .../OnlinePlay/Multiplayer/GameplayChatDisplay.cs | 3 ++- osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs index 65f667b929..c7b65856e6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/GameplayChatDisplay.cs @@ -31,14 +31,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private readonly Bindable expandedFromTextBoxFocus = new Bindable(); private const float height = 100; + private const float width = 260; public override bool PropagateNonPositionalInputSubTree => true; public GameplayChatDisplay(Room room) : base(room, leaveChannelOnDispose: false) { - RelativeSizeAxes = Axes.X; Background.Alpha = 0.2f; + Width = width; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index f60d12d84f..005cd784c4 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Screens.Select.Leaderboards; using osuTK; @@ -34,6 +35,7 @@ namespace osu.Game.Screens.Play.HUD private IGameplayLeaderboardProvider? leaderboardProvider { get; set; } private readonly IBindableList scores = new BindableList(); + private readonly Bindable configVisibility = new Bindable(); private const int max_panels = 8; @@ -64,6 +66,12 @@ namespace osu.Game.Screens.Play.HUD }; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -80,6 +88,7 @@ namespace osu.Game.Screens.Play.HUD } Scheduler.AddDelayed(sort, 1000, true); + configVisibility.BindValueChanged(_ => this.FadeTo(configVisibility.Value ? 1 : 0, 100, Easing.OutQuint), true); } /// From 78d9bd7fb4e3faea758fb1fd49184bd30442ee0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 21 Apr 2025 10:00:22 +0200 Subject: [PATCH 280/281] Fix slider repeat arrows appearing too early in editor when hit markers are enabled Closes https://github.com/ppy/osu/issues/32880 Broke in conjunction with https://github.com/ppy/osu/pull/32638 because of transforms not being applied to `DrawableSliderRepeat` but its individual pieces instead. In cross-checking with stable (visual only) the early fade in of the arrow should still apply, it just shouldn't be instantaneous as is currently ends up being with how the code is structured. --- .../Objects/Drawables/DrawableSliderRepeat.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 9368c69ebd..8205483f82 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -176,10 +176,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338) AccentColour.Value = Color4.White; Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700); + Arrow.Alpha = 0; } - Arrow.Alpha = hit ? 0 : 1; - LifetimeEnd = HitStateUpdateTime + 700; } From ec854f7b7ffeca0aec3a42b1b1355fca1ad3204c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Apr 2025 16:56:20 +0900 Subject: [PATCH 281/281] Adjust namespaces and naming --- .../TestSceneCollectionDropdown.cs | 2 +- .../TestSceneManageCollectionsDialog.cs | 2 +- .../TestSceneCollectionDropdown.cs} | 23 ++++++++++--------- .../Music/NowPlayingCollectionDropdown.cs | 2 +- .../SelectV2/CollectionDropdown.cs} | 7 +++--- 5 files changed, 19 insertions(+), 17 deletions(-) rename osu.Game.Tests/Visual/{Collections => SongSelect}/TestSceneCollectionDropdown.cs (99%) rename osu.Game.Tests/Visual/{Collections => SongSelect}/TestSceneManageCollectionsDialog.cs (99%) rename osu.Game.Tests/Visual/{Collections/TestSceneShearedCollectionDropdown.cs => SongSelectV2/TestSceneCollectionDropdown.cs} (90%) rename osu.Game/{Collections/ShearedCollectionDropdown.cs => Screens/SelectV2/CollectionDropdown.cs} (97%) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs similarity index 99% rename from osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs index a47f3c5108..fe2bf6ff5d 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneCollectionDropdown.cs @@ -22,7 +22,7 @@ using osu.Game.Tests.Resources; using osuTK.Input; using Realms; -namespace osu.Game.Tests.Visual.Collections +namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs similarity index 99% rename from osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs index 60675018e9..4c895faf27 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneManageCollectionsDialog.cs @@ -20,7 +20,7 @@ using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; -namespace osu.Game.Tests.Visual.Collections +namespace osu.Game.Tests.Visual.SongSelect { public partial class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs similarity index 90% rename from osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs index f1afdf2019..f3c96861ed 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneShearedCollectionDropdown.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneCollectionDropdown.cs @@ -21,13 +21,14 @@ using osu.Game.Rulesets; using osu.Game.Tests.Resources; using osuTK.Input; using Realms; +using CollectionDropdown = osu.Game.Screens.SelectV2.CollectionDropdown; -namespace osu.Game.Tests.Visual.Collections +namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneShearedCollectionDropdown : OsuManualInputManagerTestScene + public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene { private BeatmapManager beatmapManager = null!; - private ShearedCollectionDropdown dropdown = null!; + private CollectionDropdown dropdown = null!; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Collections { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = dropdown = new ShearedCollectionDropdown + Child = dropdown = new CollectionDropdown { Width = 300, Y = 100, @@ -84,11 +85,11 @@ namespace osu.Game.Tests.Visual.Collections AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2")))); AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3")))); - AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); + AddAssert("check count 5", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(5)); AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll())); - AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); + AddAssert("check count 2", () => dropdown.ChildrenOfType().Single().ChildrenOfType().Count(), () => Is.EqualTo(2)); } [Test] @@ -212,12 +213,12 @@ namespace osu.Game.Tests.Visual.Collections AddStep("watch for filter requests", () => { received = false; - dropdown.ChildrenOfType().First().RequestFilter = () => received = true; + dropdown.ChildrenOfType().First().RequestFilter = () => received = true; }); AddStep("click manage collections filter", () => { - int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; + int lastItemIndex = dropdown.ChildrenOfType().Single().Items.Count() - 1; InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex)); InputManager.Click(MouseButton.Left); }); @@ -237,7 +238,7 @@ namespace osu.Game.Tests.Visual.Collections private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) => AddUntilStep($"collection dropdown header displays '{collectionName}'", - () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); + () => shouldDisplay == dropdown.ChildrenOfType().Any(h => h.ChildrenOfType().Any(t => t.Text == collectionName))); private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); @@ -251,7 +252,7 @@ namespace osu.Game.Tests.Visual.Collections private void addExpandHeaderStep() => AddStep("expand header", () => { - InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); + InputManager.MoveMouseTo(dropdown.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); @@ -264,7 +265,7 @@ namespace osu.Game.Tests.Visual.Collections private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index) { // todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079 - CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); + CollectionFilterMenuItem item = dropdown.ChildrenOfType().Single().ItemSource.ElementAt(index); return dropdown.ChildrenOfType().Single(i => i.Item.Text.Value == item.CollectionName); } } diff --git a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs index 0f2e9400d9..2ba222b976 100644 --- a/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs +++ b/osu.Game/Overlays/Music/NowPlayingCollectionDropdown.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Music /// /// A for use in the . /// - public partial class NowPlayingCollectionDropdown : CollectionDropdown + public partial class NowPlayingCollectionDropdown : CollectionDropdown // TODO: class is now unused. if we decide this isn't coming back it can be nuked. { protected override bool ShowManageCollectionsItem => false; diff --git a/osu.Game/Collections/ShearedCollectionDropdown.cs b/osu.Game/Screens/SelectV2/CollectionDropdown.cs similarity index 97% rename from osu.Game/Collections/ShearedCollectionDropdown.cs rename to osu.Game/Screens/SelectV2/CollectionDropdown.cs index 2bb2f5bfe7..a2a2ec1c93 100644 --- a/osu.Game/Collections/ShearedCollectionDropdown.cs +++ b/osu.Game/Screens/SelectV2/CollectionDropdown.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -20,12 +21,12 @@ using osu.Game.Graphics.UserInterfaceV2; using osuTK; using Realms; -namespace osu.Game.Collections +namespace osu.Game.Screens.SelectV2 { /// /// A dropdown to select the collection to be used to filter results. /// - public partial class ShearedCollectionDropdown : ShearedDropdown + public partial class CollectionDropdown : ShearedDropdown // TODO: partial class under FilterControl? { /// /// Whether to show the "manage collections..." menu item in the dropdown. @@ -46,7 +47,7 @@ namespace osu.Game.Collections private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem(); - public ShearedCollectionDropdown() + public CollectionDropdown() : base("Collection") { ItemSource = filters;