diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d75f09f184..f041f2e916 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,8 +82,18 @@ jobs: run: dotnet build -c Debug -warnaserror osu.Desktop.slnf - name: Test - run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0 - shell: pwsh + run: > + dotnet test + osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll + osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll + osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll + osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll + osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll + osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll + Templates/**/*.Tests/bin/Debug/**/*.Tests.dll + --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" + -- + NUnit.ConsoleOut=0 # Attempt to upload results even if test fails. # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always @@ -136,4 +146,4 @@ jobs: run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json - name: Build - run: dotnet build -c Debug osu.iOS + run: dotnet build -c Debug osu.iOS.slnf diff --git a/osu.Android.props b/osu.Android.props index 245d49abc2..8e383a705c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + + + %(RecursiveDir)%(Filename)%(Extension) + iOS\%(RecursiveDir)%(Filename)%(Extension) + diff --git a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs index c40624a3a0..bae8e7c76a 100644 --- a/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs +++ b/osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs @@ -62,12 +62,11 @@ namespace osu.Game.Tests.Database }); }); - AddStep("Run background processor", () => - { - Add(new TestBackgroundDataStoreProcessor()); - }); + TestBackgroundDataStoreProcessor processor = null!; + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); + AddUntilStep("Wait for completion", () => processor.Completed); - AddUntilStep("wait for difficulties repopulated", () => + AddAssert("Difficulties repopulated", () => { return Realm.Run(r => { @@ -101,13 +100,10 @@ namespace osu.Game.Tests.Database }); }); - AddStep("Run background processor", () => - { - Add(new TestBackgroundDataStoreProcessor()); - }); + TestBackgroundDataStoreProcessor processor = null!; + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); AddWaitStep("wait some", 500); - AddAssert("Difficulty still not populated", () => { return Realm.Run(r => @@ -118,8 +114,9 @@ namespace osu.Game.Tests.Database }); AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying); + AddUntilStep("Wait for completion", () => processor.Completed); - AddUntilStep("wait for difficulties repopulated", () => + AddAssert("Difficulties repopulated", () => { return Realm.Run(r => { @@ -151,9 +148,11 @@ namespace osu.Game.Tests.Database }); }); - AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); + TestBackgroundDataStoreProcessor processor = null!; + AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); + AddUntilStep("Wait for completion", () => processor.Completed); - AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION)); + AddAssert("Score version upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION)); AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); } @@ -183,7 +182,7 @@ namespace osu.Game.Tests.Database AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor())); AddUntilStep("Wait for completion", () => processor.Completed); - AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); + AddAssert("Score marked as failed", () => Realm.Run(r => r.Find(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion)); } diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs index 11fa6ed92d..39de2b7bc9 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneDifficultySpectrumDisplay.cs @@ -1,12 +1,10 @@ // 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.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Beatmaps; +using osu.Framework.Testing; using osu.Game.Beatmaps.Drawables; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -15,16 +13,18 @@ namespace osu.Game.Tests.Visual.Beatmaps { public partial class TestSceneDifficultySpectrumDisplay : OsuTestScene { - private DifficultySpectrumDisplay display; + private DifficultySpectrumDisplay display = null!; - private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet + [SetUpSteps] + public void SetUpSteps() { - Beatmaps = difficulties.Select(difficulty => new APIBeatmap + AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay { - RulesetID = difficulty.rulesetId, - StarRating = difficulty.stars - }).ToArray() - }; + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(3) + }); + } [Test] public void TestSingleRuleset() @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 0, stars: 3.2), (rulesetId: 0, stars: 5.6)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 1, stars: 4.3), (rulesetId: 0, stars: 5.6)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] @@ -61,52 +61,30 @@ namespace osu.Game.Tests.Visual.Beatmaps (rulesetId: 0, stars: 5.6), (rulesetId: 15, stars: 7.8)); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] public void TestMaximumUncollapsed() { var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 12).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray()); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } [Test] public void TestMinimumCollapsed() { var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 13).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray()); - createDisplay(beatmapSet); + AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet); } - [Test] - public void TestAdjustableDotSize() + private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet { - var beatmapSet = createBeatmapSetWith( - (rulesetId: 0, stars: 2.0), - (rulesetId: 3, stars: 2.3), - (rulesetId: 0, stars: 3.2), - (rulesetId: 1, stars: 4.3), - (rulesetId: 0, stars: 5.6)); - - createDisplay(beatmapSet); - - AddStep("change dot dimensions", () => + Beatmaps = difficulties.Select(difficulty => new APIBeatmap { - display.DotSize = new Vector2(8, 12); - display.DotSpacing = 2; - }); - AddStep("change dot dimensions back", () => - { - display.DotSize = new Vector2(4, 8); - display.DotSpacing = 1; - }); - } - - private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay(beatmapSetInfo) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(3) - }); + RulesetID = difficulty.rulesetId, + StarRating = difficulty.stars + }).ToArray() + }; } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index fb9c801fb4..2f1b768ea6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -101,15 +101,13 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public void SetUpSteps() { - PlaylistItem item = null!; - AddStep("reset state", () => { multiplayerClient.Invocations.Clear(); beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable(); - item = new PlaylistItem(Beatmap.Value.BeatmapInfo) + PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; @@ -127,7 +125,7 @@ namespace osu.Game.Tests.Visual.Multiplayer multiplayerRoom = new MultiplayerRoom(0) { - Playlist = { TestMultiplayerClient.CreateMultiplayerPlaylistItem(item) }, + Playlist = { new MultiplayerPlaylistItem(item) }, Users = { localUser }, Host = localUser, }; @@ -139,8 +137,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(250, 50), - SelectedItem = new Bindable(item) + Size = new Vector2(250, 50) }; }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index c0507c184d..64cc41d0fb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -924,7 +924,7 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem( + AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { RulesetID = new OsuRuleset().RulesetInfo.OnlineID, @@ -956,7 +956,7 @@ namespace osu.Game.Tests.Visual.Multiplayer enterGameplay(); AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); - AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem( + AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) { RulesetID = new OsuRuleset().RulesetInfo.OnlineID, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs index edeb1708e0..c2d3b17ccb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -1,11 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; namespace osu.Game.Tests.Visual.Multiplayer @@ -29,10 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Height = 50, - Child = new MultiplayerMatchFooter - { - SelectedItem = new Bindable() - } + Child = new MultiplayerMatchFooter() } }; }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 7c8691d5d1..1affa08813 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () => { - MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) + MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) { Expired = expired, PlayedAt = DateTimeOffset.Now diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 1a7b677798..7283e3a1fe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("add playlist item", () => { - MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)); + MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)); MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 9e6734ce99..ff5436a87d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -5,7 +5,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.Graphics.Containers; @@ -71,15 +70,13 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(200, 50), - SelectedItem = new Bindable(room.Playlist.First()) + Size = new Vector2(200, 50) }, startControl = new MatchStartControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(200, 50), - SelectedItem = new Bindable(room.Playlist.First()) + Size = new Vector2(200, 50) } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index f7fd95a6e1..25611cf8d5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Online { Username = "flyte", Id = 3103765, - IsOnline = true, + WasRecentlyOnline = true, Statistics = new UserStatistics { GlobalRank = 1111 }, CountryCode = CountryCode.JP, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" @@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.Online { Username = "peppy", Id = 2, - IsOnline = false, + WasRecentlyOnline = false, Statistics = new UserStatistics { GlobalRank = 2222 }, CountryCode = CountryCode.AU, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", @@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Online Id = 8195163, CountryCode = CountryCode.BY, CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, + WasRecentlyOnline = false, LastVisit = DateTimeOffset.Now } }; diff --git a/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs new file mode 100644 index 0000000000..3d7ee137ba --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneImageProxying.cs @@ -0,0 +1,52 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers.Markdown; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Graphics.Containers.Markdown; +using osu.Game.Overlays.Comments; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneImageProxying : OsuTestScene + { + [Test] + public void TestExternalImageLink() + { + MarkdownContainer markdown = null!; + + // use base MarkdownContainer as a method of directly attempting to load an image without proxying logic. + AddStep("load external without proxying", () => Child = markdown = new MarkdownContainer + { + RelativeSizeAxes = Axes.Both, + Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", + }); + AddWaitStep("wait", 5); + AddAssert("image not loaded", () => markdown.ChildrenOfType().SingleOrDefault()?.Texture == null); + + AddStep("load external with proxying", () => Child = markdown = new OsuMarkdownContainer + { + RelativeSizeAxes = Axes.Both, + Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", + }); + AddUntilStep("image loaded", () => markdown.ChildrenOfType().SingleOrDefault()?.Texture != null); + } + + [Test] + public void TestExternalImageLinkInComments() + { + MarkdownContainer markdown = null!; + + AddStep("load external with proxying", () => Child = markdown = new CommentMarkdownContainer + { + RelativeSizeAxes = Axes.Both, + Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)", + }); + AddUntilStep("image loaded", () => markdown.ChildrenOfType().SingleOrDefault()?.Texture != null); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index fce888094d..29272f7336 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Online CountryCode = countryCode, CoverUrl = cover, Colour = color ?? "000000", - IsOnline = true + WasRecentlyOnline = true }; return new ClickableAvatar(user, showPanel) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index f4fc15da20..896bda364a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online Id = 3103765, CountryCode = CountryCode.JP, CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg", - IsOnline = true + WasRecentlyOnline = true }) { Width = 300 }, new UserGridPanel(new APIUser { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 6167d1f760..193b356d71 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online Id = 1001, Username = "IAmOnline", LastVisit = DateTimeOffset.Now, - IsOnline = true, + WasRecentlyOnline = true, }, new OsuRuleset().RulesetInfo)); AddStep("Show offline user", () => header.User.Value = new UserProfileData(new APIUser @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online Id = 1002, Username = "IAmOffline", LastVisit = DateTimeOffset.Now.AddDays(-10), - IsOnline = false, + WasRecentlyOnline = false, }, new OsuRuleset().RulesetInfo)); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs index f96d272e40..fb18cc8a59 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneOverallRanking.cs @@ -105,6 +105,40 @@ namespace osu.Game.Tests.Visual.Ranking displayUpdate(statistics, statistics); } + [Test] + public void TestFromNothing() + { + createDisplay(); + displayUpdate( + new UserStatistics(), + new UserStatistics + { + GlobalRank = 12_345, + Accuracy = 98.99, + MaxCombo = 2_322, + RankedScore = 23_123_543_456, + TotalScore = 123_123_543_456, + PP = 5_072 + }); + } + + [Test] + public void TestToNothing() + { + createDisplay(); + displayUpdate( + new UserStatistics + { + GlobalRank = 12_345, + Accuracy = 98.99, + MaxCombo = 2_322, + RankedScore = 23_123_543_456, + TotalScore = 123_123_543_456, + PP = 5_072 + }, + new UserStatistics()); + } + private void createDisplay() => AddStep("create display", () => { statisticsUpdate.Value = null; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index df65023303..814c0519a3 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -8,11 +8,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading; 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.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -24,8 +28,10 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; @@ -42,6 +48,22 @@ namespace osu.Game.Tests.Visual.Ranking { private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + private ScoreManager scoreManager = null!; + private RulesetStore rulesetStore = null!; + private BeatmapManager beatmapManager = null!; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); + 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.Cache(Realm); + + return dependencies; + } + [Test] public void TestScoreWithPositionStatistics() { @@ -162,6 +184,24 @@ namespace osu.Game.Tests.Visual.Ranking { var score = TestResources.CreateTestScoreInfo(); + setUpTaggingRequests(() => score.BeatmapInfo); + AddStep("load panel", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + AchievedScore = score, + } + }; + }); + } + + private void setUpTaggingRequests(Func beatmap) => AddStep("set up network requests", () => { dummyAPI.HandleRequest = request => @@ -175,7 +215,11 @@ 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 = 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.", }, ] @@ -185,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking case GetBeatmapSetRequest getBeatmapSetRequest: { - var beatmapSet = CreateAPIBeatmapSet(score.BeatmapInfo); + var beatmapSet = CreateAPIBeatmapSet(beatmap.Invoke()); beatmapSet.Beatmaps.Single().TopTags = [ new APIBeatmapTag { TagId = 3, VoteCount = 9 }, @@ -205,21 +249,6 @@ namespace osu.Game.Tests.Visual.Ranking return false; }; }); - AddStep("load panel", () => - { - Child = new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - Child = new StatisticsPanel - { - RelativeSizeAxes = Axes.Both, - State = { Value = Visibility.Visible }, - Score = { Value = score }, - AchievedScore = score, - } - }; - }); - } [Test] public void TestTaggingWhenRankTooLow() @@ -243,6 +272,100 @@ namespace osu.Game.Tests.Visual.Ranking }); } + [Test] + public void TestTaggingConvert() + { + var score = TestResources.CreateTestScoreInfo(); + score.Ruleset = new ManiaRuleset().RulesetInfo; + + AddStep("load panel", () => + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + AchievedScore = score, + } + }; + }); + } + + [Test] + public void TestTaggingInteractionWithLocalScores() + { + BeatmapInfo beatmapInfo = null!; + + AddStep(@"Import beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + }); + + AddStep("import bad score", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + score.BeatmapHash = beatmapInfo.Hash; + score.Ruleset = beatmapInfo.Ruleset; + score.Rank = ScoreRank.D; + score.User = API.LocalUser.Value; + scoreManager.Import(score); + }); + + AddStep("import score by another user", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + score.BeatmapHash = beatmapInfo.Hash; + score.Ruleset = beatmapInfo.Ruleset; + score.Rank = ScoreRank.D; + score.User = new APIUser { Username = "notme", Id = 5678 }; + scoreManager.Import(score); + }); + + AddStep("import convert score", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + score.BeatmapHash = beatmapInfo.Hash; + score.Ruleset = new OsuRuleset().RulesetInfo; + score.User = API.LocalUser.Value; + scoreManager.Import(score); + }); + + AddStep("import correct score", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + score.BeatmapHash = beatmapInfo.Hash; + score.Ruleset = beatmapInfo.Ruleset; + score.User = API.LocalUser.Value; + scoreManager.Import(score); + }); + + setUpTaggingRequests(() => beatmapInfo); + AddStep("load panel", () => + { + var score = TestResources.CreateTestScoreInfo(); + score.BeatmapInfo = beatmapInfo; + + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score }, + } + }; + }); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new StatisticsPanel diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 6eb9263c7e..499b28fb49 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -993,7 +993,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType().Single().IsScrolledToStart()); AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); - AddAssert("customisation panel closed", + AddUntilStep("customisation panel closed", () => this.ChildrenOfType().Single().ExpandedState.Value, () => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); @@ -1018,7 +1018,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void assertCustomisationToggleState(bool disabled, bool active) { AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType().Single().Enabled.Value == !disabled); - AddAssert($"customisation panel is {(active ? "" : "not ")}active", + AddUntilStep($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType().Single().ExpandedState.Value, () => active ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index fc8777068d..054bbb39d1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -196,6 +196,37 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("external overlay content still not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); } + [Test] + public void TestButtonResizedAfterFooterIsDisplayed() + { + TestShearedOverlayContainer externalOverlay = null!; + + AddStep("add overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer())); + AddStep("set buttons", () => screenFooter.SetButtons(new[] + { + new ScreenFooterButton(externalOverlay) + { + AccentColour = Dependencies.Get().Orange1, + Icon = FontAwesome.Solid.Toolbox, + Text = "One", + }, + new ScreenFooterButton { Text = "Two", Action = () => { } }, + new ScreenFooterButton { Text = "Three", Action = () => { } }, + })); + AddWaitStep("wait for transition", 3); + + AddStep("show overlay", () => externalOverlay.Show()); + AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType().Single().IsPresent); + AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.Child.Parent!.Y > 0)); + + AddStep("resize active button", () => this.ChildrenOfType().First().ResizeWidthTo(240, 300, Easing.OutQuint)); + AddStep("resize active button back", () => this.ChildrenOfType().First().ResizeWidthTo(116, 300, Easing.OutQuint)); + + AddStep("hide overlay", () => externalOverlay.Hide()); + AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType().SingleOrDefault()?.IsPresent != true); + AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType().Skip(1).All(b => b.ChildrenOfType().First().Y == 0)); + } + private partial class TestShearedOverlayContainer : ShearedOverlayContainer { public TestShearedOverlayContainer() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedDropdown.cs new file mode 100644 index 0000000000..d650ce6c36 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedDropdown.cs @@ -0,0 +1,43 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneShearedDropdown : ThemeComparisonTestScene + { + public TestSceneShearedDropdown() + : base(false) + { + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black.Opacity(0.75f), + RelativeSizeAxes = Axes.Both, + }, + new ShearedDropdown("Test") + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 300f, + Width = 140, + Current = new Bindable(), + Items = new[] { "Global", "Friends", "Local", "Really lonnnnnnng option" }, + } + } + }; + } +} diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 1d710e6395..d3ab86a8a0 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -6,9 +6,9 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Online; namespace osu.Game.Audio { @@ -30,7 +30,7 @@ namespace osu.Game.Audio [BackgroundDependencyLoader] private void load(AudioManager audioManager) { - trackStore = audioManager.GetTrackStore(new OnlineStore()); + trackStore = audioManager.GetTrackStore(new TrustedDomainOnlineStore()); } /// diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 487b578317..a6b40a26de 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -125,9 +125,10 @@ namespace osu.Game.Beatmaps /// /// Reset any fetched online linking information (and history). /// - public void ResetOnlineInfo() + public void ResetOnlineInfo(bool resetOnlineId = true) { - OnlineID = -1; + if (resetOnlineId) + OnlineID = -1; LastOnlineUpdate = null; OnlineMD5Hash = string.Empty; if (Status != BeatmapOnlineStatus.LocallyModified) diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs index a11ef0f95c..41513ec7a2 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs @@ -36,11 +36,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards Origin = Anchor.CentreLeft, TextSize = 13f }, - new DifficultySpectrumDisplay(beatmapSet) + new DifficultySpectrumDisplay { + BeatmapSet = beatmapSet, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - DotSize = new Vector2(5, 10) } } }; diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 56f6c77ba8..fc41c7c6dc 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.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.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.LocalisationExtensions; @@ -18,34 +17,6 @@ namespace osu.Game.Beatmaps.Drawables { public partial class DifficultySpectrumDisplay : CompositeDrawable { - private Vector2 dotSize = new Vector2(4, 8); - - public Vector2 DotSize - { - get => dotSize; - set - { - dotSize = value; - - if (IsLoaded) - updateDisplay(); - } - } - - private float dotSpacing = 1; - - public float DotSpacing - { - get => dotSpacing; - set - { - dotSpacing = value; - - if (IsLoaded) - updateDisplay(); - } - } - private IBeatmapSetInfo? beatmapSet; public IBeatmapSetInfo? BeatmapSet @@ -60,9 +31,12 @@ namespace osu.Game.Beatmaps.Drawables } } - private readonly FillFlowContainer flow; + private FillFlowContainer flow = null!; - public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null) + private const int max_difficulties_before_collapsing = 12; + + [BackgroundDependencyLoader] + private void load() { AutoSizeAxes = Axes.Both; @@ -72,8 +46,6 @@ namespace osu.Game.Beatmaps.Drawables Spacing = new Vector2(10, 0), Direction = FillDirection.Horizontal, }; - - BeatmapSet = beatmapSet; } protected override void LoadComplete() @@ -84,36 +56,70 @@ namespace osu.Game.Beatmaps.Drawables private void updateDisplay() { - flow.Clear(); + foreach (var group in flow) + group.Alpha = 0; if (beatmapSet == null) + { + foreach (var group in flow) + group.Beatmaps = []; return; + } // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 - bool collapsed = beatmapSet.Beatmaps.Count() > 12; + bool collapsed = beatmapSet.Beatmaps.Count() > max_difficulties_before_collapsing; foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize) + int rulesetId = rulesetGrouping.Key.OnlineID; + + var group = flow.SingleOrDefault(rg => rg.RulesetId == rulesetId); + + if (group == null) { - Spacing = new Vector2(DotSpacing, 0f), - }); + group = new RulesetDifficultyGroup(rulesetId); + flow.Add(group); + flow.SetLayoutPosition(group, rulesetId); + } + + group.Alpha = 1; + group.Beatmaps = rulesetGrouping.ToArray(); + group.Collapsed = collapsed; } } private partial class RulesetDifficultyGroup : FillFlowContainer { - private readonly int rulesetId; - private readonly IEnumerable beatmapInfos; - private readonly bool collapsed; - private readonly Vector2 dotSize; + public readonly int RulesetId; - public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed, Vector2 dotSize) + private IBeatmapInfo[] beatmaps = []; + + public IBeatmapInfo[] Beatmaps { - this.rulesetId = rulesetId; - this.beatmapInfos = beatmapInfos; - this.collapsed = collapsed; - this.dotSize = dotSize; + set + { + beatmaps = value.OrderBy(bi => bi.StarRating).ToArray(); + updateDisplay(); + } + } + + private bool collapsed; + + public bool Collapsed + { + get => collapsed; + set + { + collapsed = value; + updateDisplay(); + } + } + + private OsuSpriteText countText = null!; + + public RulesetDifficultyGroup(int rulesetId) + { + RulesetId = rulesetId; } [BackgroundDependencyLoader] @@ -123,53 +129,83 @@ namespace osu.Game.Beatmaps.Drawables Spacing = new Vector2(1, 0); Direction = FillDirection.Horizontal; - var icon = rulesets.GetRuleset(rulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; + var icon = rulesets.GetRuleset(RulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; Add(icon.With(i => { i.Size = new Vector2(14); i.Anchor = i.Origin = Anchor.Centre; })); - if (!collapsed) + for (int i = 0; i < max_difficulties_before_collapsing; i++) + Add(new DifficultyDot()); + + Add(countText = new OsuSpriteText { - foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating)) - Add(new DifficultyDot(beatmapInfo.StarRating, dotSize)); - } - else + Font = OsuFont.Default.With(size: 12), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Bottom = 1 } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateDisplay(); + } + + private void updateDisplay() + { + countText.Alpha = collapsed ? 1 : 0; + countText.Text = beatmaps.Length.ToLocalisableString(@"N0"); + + var dots = this.OfType().ToArray(); + + for (int i = 0; i < max_difficulties_before_collapsing; i++) { - Add(new OsuSpriteText + var dot = dots[i]; + + if (collapsed || i >= beatmaps.Length) { - Text = beatmapInfos.Count().ToLocalisableString(@"N0"), - Font = OsuFont.Default.With(size: 12), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding { Bottom = 1 } - }); + dot.Alpha = 0; + continue; + } + + dot.Alpha = 1; + dot.StarDifficulty = beatmaps[i].StarRating; } } } - private partial class DifficultyDot : CircularContainer + private partial class DifficultyDot : Circle { - private readonly double starDifficulty; + private double starDifficulty; - public DifficultyDot(double starDifficulty, Vector2 dotSize) + public double StarDifficulty { - this.starDifficulty = starDifficulty; - Size = dotSize; + get => starDifficulty; + set + { + starDifficulty = value; + updateColour(); + } } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Anchor = Origin = Anchor.Centre; - Masking = true; + [Resolved] + private OsuColour colours { get; set; } = null!; - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(starDifficulty) - }; + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(5, 10); + Anchor = Origin = Anchor.Centre; + + updateColour(); + } + + private void updateColour() + { + Colour = colours.ForStarDifficulty(starDifficulty); } } } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 3212e17b7b..7142f2b300 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -97,8 +97,9 @@ namespace osu.Game.Database /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. /// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯. /// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions. + /// 48 2025-03-19 Clear online status for all qualified beatmaps (some were stuck in a qualified state due to local caching issues). /// - private const int schema_version = 47; + private const int schema_version = 48; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1245,6 +1246,15 @@ namespace osu.Game.Database break; } + + case 48: + const int qualified = (int)BeatmapOnlineStatus.Qualified; + + var beatmaps = migration.NewRealm.All().Where(b => b.StatusInt == qualified); + + foreach (var beatmap in beatmaps) + beatmap.ResetOnlineInfo(resetOnlineId: false); + break; } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index e877915fac..d22aa197bb 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -127,8 +127,6 @@ namespace osu.Game.Graphics.Backgrounds { base.Update(); - Invalidate(Invalidation.DrawNode); - if (CreateNewTriangles) addTriangles(false); @@ -138,6 +136,10 @@ namespace osu.Game.Graphics.Backgrounds : 1; float elapsedSeconds = (float)Time.Elapsed / 1000; + + if (elapsedSeconds == 0) + return; + // Since position is relative, the velocity needs to scale inversely with DrawHeight. // Since we will later multiply by the scale of individual triangles we normalize by // dividing by triangleScale. @@ -157,6 +159,8 @@ namespace osu.Game.Graphics.Backgrounds if (bottomPos < 0) parts.RemoveAt(i); } + + Invalidate(Invalidation.DrawNode); } /// @@ -183,8 +187,13 @@ namespace osu.Game.Graphics.Backgrounds int currentCount = parts.Count; + if (AimCount - currentCount == 0) + return; + for (int i = 0; i < AimCount - currentCount; i++) parts.Add(createTriangle(randomY)); + + Invalidate(Invalidation.DrawNode); } private TriangleParticle createTriangle(bool randomY) diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs index 4143a6d76d..358e859cc8 100644 --- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs +++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs @@ -91,12 +91,14 @@ namespace osu.Game.Graphics.Backgrounds { base.Update(); - Invalidate(Invalidation.DrawNode); - if (CreateNewTriangles) addTriangles(false); float elapsedSeconds = (float)Time.Elapsed / 1000; + + if (elapsedSeconds == 0) + return; + // Since position is relative, the velocity needs to scale inversely with DrawHeight. float movedDistance = -elapsedSeconds * Velocity * base_velocity / DrawHeight; @@ -112,6 +114,8 @@ namespace osu.Game.Graphics.Backgrounds if (bottomPos < 0) parts.RemoveAt(i); } + + Invalidate(Invalidation.DrawNode); } /// @@ -138,8 +142,13 @@ namespace osu.Game.Graphics.Backgrounds int currentCount = parts.Count; + if (AimCount - currentCount == 0) + return; + for (int i = 0; i < AimCount - currentCount; i++) parts.Add(createTriangle(randomY)); + + Invalidate(Invalidation.DrawNode); } private TriangleParticle createTriangle(bool randomY) diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs index 10207dd389..340e59dd91 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownImage.cs @@ -13,7 +13,7 @@ namespace osu.Game.Graphics.Containers.Markdown public LocalisableString TooltipText { get; } public OsuMarkdownImage(LinkInline linkInline) - : base(linkInline.Url) + : base($"https://osu.ppy.sh/media-url?url={linkInline.Url}") { TooltipText = linkInline.Title; } diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index dc42216c55..5a1fbaa3a4 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface #region OsuDropdownMenu - protected partial class OsuDropdownMenu : DropdownMenu + public partial class OsuDropdownMenu : DropdownMenu { public override bool HandleNonPositionalInput => State == MenuState.Open; diff --git a/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs new file mode 100644 index 0000000000..0b9c5f294c --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/ShearedDropdown.cs @@ -0,0 +1,308 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +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.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class ShearedDropdown : Dropdown, IKeyBindingHandler + { + protected override DropdownHeader CreateHeader() => new ShearedDropdownHeader(); + + protected override DropdownMenu CreateMenu() => new ShearedDropdownMenu(); + + public ShearedDropdown(LocalisableString label) + { + if (Header is ShearedDropdownHeader osuHeader) + { + osuHeader.Dropdown = this; + osuHeader.LeftSideLabel = label; + } + } + + 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; + + if (e.Action == GlobalAction.Back) + return Back(); + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + protected partial class ShearedDropdownMenu : OsuDropdown.OsuDropdownMenu + { + private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); + + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + public ShearedDropdownMenu() + { + Shear = shear; + Margin = new MarginPadding { Top = 5f }; + } + + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item) + { + BackgroundColourHover = HoverColour, + BackgroundColourSelected = SelectionColour + }; + + public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem + { + private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); + + public ShearedMenuItem(MenuItem item) + : base(item) + { + Foreground.Shear = -shear; + } + } + } + + public partial class ShearedDropdownHeader : DropdownHeader + { + private const float corner_radius = 5f; + + private LocalisableString label; + + protected override LocalisableString Label + { + get => label; + set + { + label = value; + valueText.Text = value; + } + } + + public LocalisableString LeftSideLabel + { + set => labelText.Text = value; + } + + private readonly OsuSpriteText labelText; + private readonly OsuSpriteText valueText; + private readonly Box labelBox; + private readonly SpriteIcon chevron; + + public Container LabelContainer { get; } + + 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; + CornerRadius = corner_radius; + Masking = true; + + Foreground.Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new[] + { + LabelContainer = new Container + { + CornerRadius = corner_radius, + Masking = true, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + labelBox = new Box + { + RelativeSizeAxes = Axes.Both + }, + labelText = new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f }, + Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + Shear = -shear, + }, + }, + }, + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10f }, + Shear = -shear, + Children = new Drawable[] + { + valueText = new TruncatingSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = 15f }, + Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + chevron = new SpriteIcon + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Y = 1f, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(10f), + } + }, + }, + } + } + }, + }; + + AddInternal(LabelContainer.CreateProxy()); + } + + [BackgroundDependencyLoader] + private void load() + { + labelBox.Colour = colourProvider.Background3; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Dropdown.Menu.StateChanged += _ => updateChevron(); + SearchBar.State.ValueChanged += _ => updateColour(); + Enabled.BindValueChanged(_ => updateColour()); + updateColour(); + } + + protected override void Update() + { + base.Update(); + 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 }; + } + + protected override bool OnHover(HoverEvent e) + { + updateColour(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateColour(); + } + + private void updateColour() + { + bool hovered = Enabled.Value && IsHovered; + var hoveredColour = colourProvider.Light4; + var unhoveredColour = colourProvider.Background5; + + Colour = Color4.White; + Alpha = Enabled.Value ? 1 : 0.3f; + + if (SearchBar.State.Value == Visibility.Visible) + { + chevron.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White; + Background.Colour = unhoveredColour; + } + else + { + chevron.Colour = Color4.White; + Background.Colour = hovered ? hoveredColour : unhoveredColour; + } + } + + private void updateChevron() + { + Debug.Assert(Dropdown != null); + bool open = Dropdown.Menu.State == MenuState.Open; + chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); + } + + protected override DropdownSearchBar CreateSearchBar() => searchBar = new ShearedDropdownSearchBar(); + + private partial class ShearedDropdownSearchBar : DropdownSearchBar + { + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); + + protected override TextBox CreateTextBox() => new DropdownSearchTextBox + { + FontSize = OsuFont.Default.Size, + }; + + private partial class DropdownSearchTextBox : OsuTextBox + { + private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0); + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider? colourProvider) + { + TextContainer.Shear = -shear; + BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255); + } + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + BorderThickness = 0; + } + } + } + } + } +} diff --git a/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs index d3e8c0f8c8..22f9fe6d02 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableModDisplayStrings.cs @@ -17,7 +17,13 @@ namespace osu.Game.Localisation.SkinComponents /// /// "Whether to show extended information for each mod." /// - public static LocalisableString ShowExtendedInformationDescription => new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod."); + public static LocalisableString ShowExtendedInformationDescription => + new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod."); + + /// + /// "Display direction" + /// + public static LocalisableString DisplayDirection => new TranslatableString(getKey(@"display_direction"), "Display direction"); /// /// "Expansion mode" diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 92b7d9d874..4e219cdf22 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -9,6 +9,7 @@ using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Game.Extensions; +using osu.Game.Online.Metadata; using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses @@ -111,8 +112,13 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"is_active")] public bool Active; + /// + /// From osu-web's perspective, whether a user was recently online. + /// This doesn't imply the user is online in a lazer client (may be updated from stable or web browser). + /// Use for real-time lazer online status checks. + /// [JsonProperty(@"is_online")] - public bool IsOnline; + public bool WasRecentlyOnline; [JsonProperty(@"pm_friends_only")] public bool PMFriendsOnly; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs index ed3ee4d45e..ee497bf3fd 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.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.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Containers; @@ -219,7 +220,7 @@ namespace osu.Game.Online.Leaderboards } }; - string description = mod.SettingDescription; + string description = string.Join(", ", mod.SettingDescription.Select(svp => $"{svp.setting}: {svp.value}")); if (!string.IsNullOrEmpty(description)) { @@ -227,7 +228,7 @@ namespace osu.Game.Online.Leaderboards { RelativeSizeAxes = Axes.Y, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = mod.SettingDescription, + Text = description, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, Margin = new MarginPadding { Top = 1 }, diff --git a/osu.Game/Online/Metadata/MetadataClient.cs b/osu.Game/Online/Metadata/MetadataClient.cs index 9885419b65..0679191a52 100644 --- a/osu.Game/Online/Metadata/MetadataClient.cs +++ b/osu.Game/Online/Metadata/MetadataClient.cs @@ -57,6 +57,9 @@ namespace osu.Game.Online.Metadata /// /// Attempts to retrieve the presence of a user. /// + /// + /// This will return data if the client is currently receiving presence data. See . + /// /// The user ID. /// The user presence, or null if not available or the user's offline. public UserPresence? GetPresence(int userId) diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs index 59a12b3bf1..757bb07ec8 100644 --- a/osu.Game/Online/SignalRWorkaroundTypes.cs +++ b/osu.Game/Online/SignalRWorkaroundTypes.cs @@ -44,6 +44,8 @@ namespace osu.Game.Online (typeof(UserActivity.EditingBeatmap), typeof(UserActivity)), (typeof(UserActivity.ModdingBeatmap), typeof(UserActivity)), (typeof(UserActivity.TestingBeatmap), typeof(UserActivity)), + (typeof(UserActivity.InDailyChallengeLobby), typeof(UserActivity)), + (typeof(UserActivity.PlayingDailyChallenge), typeof(UserActivity)), }; } } diff --git a/osu.Game/Online/TrustedDomainOnlineStore.cs b/osu.Game/Online/TrustedDomainOnlineStore.cs new file mode 100644 index 0000000000..2b47f159e6 --- /dev/null +++ b/osu.Game/Online/TrustedDomainOnlineStore.cs @@ -0,0 +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 osu.Framework.IO.Stores; +using osu.Framework.Logging; + +namespace osu.Game.Online +{ + public sealed class TrustedDomainOnlineStore : OnlineStore + { + protected override string GetLookupUrl(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase)) + { + Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important); + return string.Empty; + } + + return url; + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7d35207bbe..4087a8b71e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -108,6 +108,8 @@ namespace osu.Game public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); + protected override OnlineStore CreateOnlineStore() => new TrustedDomainOnlineStore(); + public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); /// @@ -278,7 +280,7 @@ namespace osu.Game dependencies.CacheAs(Storage); var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); - largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore())); + largeStore.AddTextureSource(Host.CreateTextureLoaderStore(CreateOnlineStore())); dependencies.Cache(largeStore); dependencies.CacheAs(LocalConfig); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 9b9661f83d..cc06383274 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -249,6 +249,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores getScoresRequest = null; noScoresPlaceholder.Hide(); + noTeamPlaceholder.Hide(); + notSupporterPlaceholder.Hide(); if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.Status <= BeatmapOnlineStatus.Pending)) { @@ -271,9 +273,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; } - noTeamPlaceholder.Hide(); - notSupporterPlaceholder.Hide(); - Show(); loading.Show(); diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs index 83f67d1a8a..57338dde9f 100644 --- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs +++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -14,6 +15,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -22,7 +24,10 @@ using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; +using osu.Game.Online.Multiplayer; using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens; +using osu.Game.Screens.Play; using osuTK; using osuTK.Graphics; using ChatStrings = osu.Game.Localisation.ChatStrings; @@ -69,6 +74,12 @@ namespace osu.Game.Overlays.Chat [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private MultiplayerClient? multiplayerClient { get; set; } + + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + [Resolved(canBeNull: true)] private ChannelManager? chatManager { get; set; } @@ -161,13 +172,10 @@ namespace osu.Game.Overlays.Chat if (user.Equals(APIUser.SYSTEM_USER)) return Array.Empty(); - List items = new List - { - new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile) - }; + if (user.Equals(api.LocalUser.Value)) + return Array.Empty(); - if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); + List items = new List(); if (currentChannel?.Value != null) { @@ -177,8 +185,29 @@ namespace osu.Game.Overlays.Chat })); } - if (!user.Equals(api.LocalUser.Value)) - items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); + items.Add(new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile)); + + items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel)); + + // We should probably be checking against an online state here. + // But we can't use MetadataClient.GetPresence because we may not be requesting/receiving presences. + // This isn't really too bad – worst case scenario the client will open spectator view and show the user as "offline". + { + items.Add(new OsuMenuItemSpacer()); + + items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () => + { + performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(user))); + })); + + if (multiplayerClient?.Room?.Users.All(u => u.UserID != user.Id) == true) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(user.Id))); + } + } + + items.Add(new OsuMenuItemSpacer()); + items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested)); return items.ToArray(); } diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 526ab6fc63..8014126942 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Database; using osu.Game.Graphics; @@ -75,17 +76,31 @@ namespace osu.Game.Overlays.Mods TabbableContentContainer = this, Current = { Value = preset.PerformRead(p => p.Description) }, }, - new OsuScrollContainer + new Container { RelativeSizeAxes = Axes.X, Height = 100, - Padding = new MarginPadding(7), - Child = scrollContent = new FillFlowContainer + CornerRadius = 10, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(7), - Spacing = new Vector2(7), + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(7), + Child = scrollContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(7), + Spacing = new Vector2(7), + } + }, } }, new FillFlowContainer diff --git a/osu.Game/Overlays/Mods/ModPresetRow.cs b/osu.Game/Overlays/Mods/ModPresetRow.cs index 4829e93b87..408c541bf5 100644 --- a/osu.Game/Overlays/Mods/ModPresetRow.cs +++ b/osu.Game/Overlays/Mods/ModPresetRow.cs @@ -1,10 +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.Linq; +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.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -14,12 +15,20 @@ namespace osu.Game.Overlays.Mods { public partial class ModPresetRow : FillFlowContainer { + private readonly Mod mod; + public ModPresetRow(Mod mod) + { + this.mod = mod; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; - Spacing = new Vector2(4); + Spacing = new Vector2(5); InternalChildren = new Drawable[] { new FillFlowContainer @@ -39,26 +48,47 @@ namespace osu.Game.Overlays.Mods }, new OsuSpriteText { - Text = mod.Name, - Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold), - Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Bottom = 2 } - } + Origin = Anchor.CentreLeft, + Font = OsuFont.Torus.With(size: 16f, weight: FontWeight.SemiBold), + Colour = colourProvider.Content1, + UseFullGlyphHeight = false, + Text = mod.Name, + }, } - } - }; - - if (!string.IsNullOrEmpty(mod.SettingDescription)) - { - AddInternal(new OsuTextFlowContainer + }, + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 14 }, - Text = mod.SettingDescription - }); - } + Padding = new MarginPadding { Horizontal = 10f }, + Alpha = mod.SettingDescription.Any() ? 1 : 0, + Children = new Drawable[] + { + new TextFlowContainer(t => + { + t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold); + }) + { + AutoSizeAxes = Axes.Both, + Colour = colourProvider.Content2, + Text = string.Join('\n', mod.SettingDescription.Select(svp => svp.setting)), + }, + new TextFlowContainer(t => + { + t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold); + }) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Colour = colourProvider.Content1, + TextAnchor = Anchor.TopRight, + Text = string.Join('\n', mod.SettingDescription.Select(svp => svp.value)), + }, + } + } + }; } } } diff --git a/osu.Game/Overlays/Mods/ModPresetTooltip.cs b/osu.Game/Overlays/Mods/ModPresetTooltip.cs index 6ffcfca1e0..4464ba22f1 100644 --- a/osu.Game/Overlays/Mods/ModPresetTooltip.cs +++ b/osu.Game/Overlays/Mods/ModPresetTooltip.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -14,6 +15,9 @@ namespace osu.Game.Overlays.Mods { public partial class ModPresetTooltip : VisibilityContainer, ITooltip { + [Cached] + private readonly OverlayColourProvider colourProvider; + protected override Container Content { get; } private const double transition_duration = 200; @@ -22,6 +26,8 @@ namespace osu.Game.Overlays.Mods public ModPresetTooltip(OverlayColourProvider colourProvider) { + this.colourProvider = colourProvider; + Width = 250; AutoSizeAxes = Axes.Y; @@ -39,7 +45,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 }, + Padding = new MarginPadding(10f), Spacing = new Vector2(7), Children = new[] { @@ -51,6 +57,7 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 5f }, } } } @@ -64,7 +71,13 @@ namespace osu.Game.Overlays.Mods if (ReferenceEquals(preset, lastPreset)) return; - descriptionText.Text = preset.Description; + if (!string.IsNullOrEmpty(preset.Description)) + { + descriptionText.Show(); + descriptionText.Text = preset.Description; + } + else + descriptionText.Hide(); lastPreset = preset; diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 87920fdf55..da5388534c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -20,6 +20,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets.Mods; +using osu.Game.Seasonal; namespace osu.Game.Overlays { @@ -256,8 +257,8 @@ namespace osu.Game.Overlays playableSet = getNextRandom(-1, allowProtectedTracks); else { - playableSet = getBeatmapSets().TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Value.Protected || allowProtectedTracks) - ?? getBeatmapSets().LastOrDefault(s => !s.Value.Protected || allowProtectedTracks); + playableSet = getBeatmapSets(allowProtectedTracks).TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault() + ?? getBeatmapSets(allowProtectedTracks).LastOrDefault(); } if (playableSet != null) @@ -352,10 +353,8 @@ namespace osu.Game.Overlays playableSet = getNextRandom(1, allowProtectedTracks); else { - playableSet = getBeatmapSets().SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)) - .Where(i => !i.Value.Protected || allowProtectedTracks) - .ElementAtOrDefault(1) - ?? getBeatmapSets().FirstOrDefault(i => !i.Value.Protected || allowProtectedTracks); + playableSet = getBeatmapSets(allowProtectedTracks).SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).ElementAtOrDefault(1) + ?? getBeatmapSets(allowProtectedTracks).FirstOrDefault(); } var playableBeatmap = playableSet?.Value.Beatmaps.FirstOrDefault(); @@ -376,12 +375,13 @@ namespace osu.Game.Overlays { Live result; - var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToList(); + 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. + // 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(); @@ -459,9 +459,12 @@ namespace osu.Game.Overlays private TrackChangeDirection? queuedDirection; - private IEnumerable> getBeatmapSets() => realm.Realm.All().Where(s => !s.DeletePending) - .AsEnumerable() - .Select(s => new RealmLive(s, realm)); + private IEnumerable> getBeatmapSets(bool allowProtectedTracks) => + realm.Realm.All().Where(s => !s.DeletePending) + .AsEnumerable() + .Select(s => new RealmLive(s, realm)) + .Where(i => (allowProtectedTracks || !i.Value.Protected) + && (SeasonalUIConfig.ENABLED || i.Value.Hash != IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH)); private void changeBeatmap(WorkingBeatmap newWorking) { @@ -488,8 +491,8 @@ namespace osu.Game.Overlays else { // figure out the best direction based on order in playlist. - int last = getBeatmapSets().TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count(); - int next = getBeatmapSets().TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count(); + int last = getBeatmapSets(allowProtectedTracks: false).TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count(); + int next = getBeatmapSets(allowProtectedTracks: false).TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count(); direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index 03c849052b..db93ec7e05 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Profile.Header addSpacer(topLinkContainer); - if (user.IsOnline) + if (user.WasRecentlyOnline) { topLinkContainer.AddText(UsersStrings.ShowLastvisitOnline); addSpacer(topLinkContainer); diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index dd41f156f3..b1a0ca0ccd 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -3,14 +3,13 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Events; -using osu.Framework.Layout; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -29,8 +28,6 @@ namespace osu.Game.Overlays private const int header_height = 30; private const int corner_radius = 5; - private readonly Cached headerTextVisibilityCache = new Cached(); - protected override Container Content => content; private readonly FillFlowContainer content = new FillFlowContainer @@ -156,13 +153,9 @@ namespace osu.Game.Overlays { base.Update(); - if (!headerTextVisibilityCache.IsValid) - { - // These toolbox grouped may be contracted to only show icons. - // For now, let's hide the header to avoid text truncation weirdness in such cases. - headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); - headerTextVisibilityCache.Validate(); - } + // These toolbox grouped may be contracted to only show icons. + // For now, let's hide the header to avoid text truncation weirdness in such cases. + headerText.Alpha = (float)Interpolation.DampContinuously(headerText.Alpha, headerText.DrawWidth < DrawWidth ? 1 : 0, 40, Time.Elapsed); // Dragged child finished its drag operation. if (draggedChild != null && inputManager.DraggedDrawable != draggedChild) @@ -172,14 +165,6 @@ namespace osu.Game.Overlays } } - protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) - { - if (invalidation.HasFlag(Invalidation.DrawSize)) - headerTextVisibilityCache.Invalidate(); - - return base.OnInvalidate(invalidation, source); - } - private void updateExpandedState(bool animate) { // before we collapse down, let's double check the user is not dragging a UI control contained within us. diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 1b21216235..f23f16fd44 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Extensions; -using osu.Game.Rulesets.UI; using osu.Game.Utils; namespace osu.Game.Rulesets.Mods @@ -43,36 +42,16 @@ namespace osu.Game.Rulesets.Mods public abstract LocalisableString Description { get; } /// - /// The tooltip to display for this mod when used in a . - /// - /// - /// Differs from , as the value of attributes (AR, CS, etc) changeable via the mod - /// are displayed in the tooltip. - /// - [JsonIgnore] - public string IconTooltip - { - get - { - string description = SettingDescription; - - return string.IsNullOrEmpty(description) ? Name : $"{Name} ({description})"; - } - } - - /// - /// The description of editable settings of a mod to use in the . + /// The description of editable settings of a mod. /// /// /// Parentheses are added to the tooltip, surrounding the value of this property. If this property is string.Empty, /// the tooltip will not have parentheses. /// - public virtual string SettingDescription + public virtual IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get { - var tooltipTexts = new List(); - foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { var bindable = (IBindable)property.GetValue(this)!; @@ -82,7 +61,7 @@ namespace osu.Game.Rulesets.Mods switch (bindable) { case Bindable b: - valueText = b.Value ? "on" : "off"; + valueText = b.Value ? "On" : "Off"; break; default: @@ -91,10 +70,8 @@ namespace osu.Game.Rulesets.Mods } if (!bindable.IsDefault) - tooltipTexts.Add($"{attr.Label}: {valueText}"); + yield return (attr.Label, valueText); } - - return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index 9570cddb0a..db16e771d3 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Globalization; +using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Localisation.HUD; @@ -33,7 +34,20 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; - public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo)); + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + if (!MinimumAccuracy.IsDefault) + yield return ("Minimum accuracy", $"{MinimumAccuracy.Value:##%}"); + + if (!AccuracyJudgeMode.IsDefault) + yield return ("Accuracy mode", AccuracyJudgeMode.Value.ToLocalisableString()); + + if (!Restart.IsDefault) + yield return ("Restart on fail", "On"); + } + } [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider))] public BindableNumber MinimumAccuracy { get; } = new BindableDouble diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 67f9da37be..ceaa9aa6e5 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -38,7 +39,14 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "The whole playfield is on a wheel!"; public override double ScoreMultiplier => 1; - public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}"; + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); + yield return ("Direction", Direction.Value.GetDescription()); + } + } private PlayfieldAdjustmentContainer playfieldAdjustmentContainer = null!; diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index f4c6be4f77..cdde1b73b6 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -65,18 +65,15 @@ namespace osu.Game.Rulesets.Mods } } - public override string SettingDescription + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription { get { - string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:N1}"; - string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:N1}"; + if (!DrainRate.IsDefault) + yield return ("HP drain", $"{DrainRate.Value:N1}"); - return string.Join(", ", new[] - { - drainRate, - overallDifficulty - }.Where(s => !string.IsNullOrEmpty(s))); + if (!OverallDifficulty.IsDefault) + yield return ("Accuracy", $"{OverallDifficulty.Value:N1}"); } } diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index e101ac440e..1a2cb08a53 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using Humanizer; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; @@ -20,7 +22,15 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; - public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + if (!Retries.IsDefault) + yield return ("Extra lives", "lives".ToQuantity(Retries.Value)); + } + } + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray(); private int retries; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index e5af758b4f..358034541c 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { @@ -24,8 +26,13 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp), typeof(ModAdaptiveSpeed), typeof(ModRateAdjust) }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; - - public override string ExtendedIconInformation => SettingDescription; + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + if (!SpeedChange.IsDefault) + yield return ("Speed change", $"{SpeedChange.Value:N2}x"); + } + } } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 36e4522771..fd85709b52 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Overlays.Settings; @@ -34,7 +36,13 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; - public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; + public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription + { + get + { + yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); + } + } private double finalRateTime; private double beginRampTime; diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 6abc7355d5..ee0103a8e5 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -10,11 +10,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.UI /// /// Display the specified mod at a fixed size. /// - public partial class ModIcon : Container, IHasTooltip + public partial class ModIcon : Container, IHasCustomTooltip { public readonly BindableBool Selected = new BindableBool(); @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.UI public static readonly Vector2 MOD_ICON_SIZE = new Vector2(80); - public virtual LocalisableString TooltipText => showTooltip ? ((mod as Mod)?.IconTooltip ?? mod.Name) : string.Empty; + public Mod? TooltipContent { get; private set; } private IMod mod; @@ -70,6 +70,9 @@ namespace osu.Game.Rulesets.UI [Resolved] private OsuColour colours { get; set; } = null!; + [Resolved] + private OverlayColourProvider? colourProvider { get; set; } + private Color4 backgroundColour; private Sprite extendedBackground = null!; @@ -188,6 +191,7 @@ namespace osu.Game.Rulesets.UI modAcronym.Text = value.Acronym; modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question; + TooltipContent = showTooltip ? value as Mod : null; if (value.Icon == null) { @@ -227,5 +231,7 @@ namespace osu.Game.Rulesets.UI base.Dispose(isDisposing); modSettingsChangeTracker?.Dispose(); } + + public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); } } diff --git a/osu.Game/Rulesets/UI/ModTooltip.cs b/osu.Game/Rulesets/UI/ModTooltip.cs new file mode 100644 index 0000000000..07bb30e15a --- /dev/null +++ b/osu.Game/Rulesets/UI/ModTooltip.cs @@ -0,0 +1,141 @@ +// 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.Color4Extensions; +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.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.UI +{ + public partial class ModTooltip : VisibilityContainer, ITooltip + { + private readonly OverlayColourProvider colourProvider; + + private OsuSpriteText nameText = null!; + private TextFlowContainer settingsLabelsFlow = null!; + private TextFlowContainer settingsValuesFlow = null!; + + public ModTooltip(OverlayColourProvider? colourProvider = null) + { + this.colourProvider = colourProvider ?? new OverlayColourProvider(OverlayColourScheme.Aquamarine); + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + CornerRadius = 7; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.2f), + Radius = 10f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding(10f), + Spacing = new Vector2(20f, 0f), + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + nameText = new OsuSpriteText + { + Font = OsuFont.Torus.With(size: 16f, weight: FontWeight.SemiBold), + Colour = colourProvider.Content1, + UseFullGlyphHeight = false, + }, + settingsLabelsFlow = new TextFlowContainer(t => + { + t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold); + }) + { + AutoSizeAxes = Axes.Both, + Colour = colourProvider.Content2, + }, + }, + }, + settingsValuesFlow = new TextFlowContainer(t => + { + t.Font = OsuFont.Torus.With(size: 12f, weight: FontWeight.SemiBold); + }) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Colour = colourProvider.Content1, + TextAnchor = Anchor.TopRight, + }, + }, + } + }; + } + + private Mod? displayedContent; + + public void SetContent(Mod content) + { + if (content == displayedContent) + return; + + displayedContent = content; + nameText.Text = content.Name; + settingsLabelsFlow.Clear(); + settingsValuesFlow.Clear(); + + if (content.SettingDescription.Any()) + { + settingsLabelsFlow.Show(); + settingsValuesFlow.Show(); + + foreach (var part in content.SettingDescription) + { + settingsLabelsFlow.AddText(part.setting); + settingsLabelsFlow.NewLine(); + + settingsValuesFlow.AddText(part.value); + settingsValuesFlow.NewLine(); + } + } + else + { + settingsLabelsFlow.Hide(); + settingsValuesFlow.Hide(); + } + } + + protected override void PopIn() => this.FadeIn(300, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(300, Easing.OutQuint); + public void Move(Vector2 pos) => Position = pos; + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index b8f2695259..22df917992 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -146,22 +146,11 @@ namespace osu.Game.Screens.Edit.Compose.Components } } }, - new Drawable[] - { - new TextFlowContainer(s => s.Font = s.Font.With(size: 14)) - { - Padding = new MarginPadding { Horizontal = 15, Vertical = 2 }, - Text = "beat snap", - RelativeSizeAxes = Axes.X, - TextAnchor = Anchor.TopCentre, - }, - }, }, RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, 40), new Dimension(GridSizeMode.Absolute, 20), - new Dimension(GridSizeMode.Absolute, 15) } } }; diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index ea32507ca0..f75250a832 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -33,7 +33,8 @@ namespace osu.Game.Screens.Footer private Box background = null!; private FillFlowContainer buttonsFlow = null!; - private Container removedButtonsContainer = null!; + private Container footerContentContainer = null!; + private Container hiddenButtonsContainer = null!; private LogoTrackingContainer logoTrackingContainer = null!; [Cached] @@ -71,15 +72,35 @@ namespace osu.Game.Screens.Footer RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5 }, - buttonsFlow = new FillFlowContainer + new GridContainer { - Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, - Y = 10f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(7, 0), - AutoSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + buttonsFlow = new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = 10f, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(7, 0), + AutoSizeAxes = Axes.Both, + }, + footerContentContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Y = -15f, + }, + }, + } }, BackButton = new ScreenBackButton { @@ -88,7 +109,7 @@ namespace osu.Game.Screens.Footer Origin = Anchor.BottomLeft, Action = onBackPressed, }, - removedButtonsContainer = new Container + hiddenButtonsContainer = new Container { Margin = new MarginPadding { Left = 12f + ScreenBackButton.BUTTON_WIDTH + padding }, Y = 10f, @@ -153,7 +174,7 @@ namespace osu.Game.Screens.Footer var oldButton = oldButtons[i]; buttonsFlow.Remove(oldButton, false); - removedButtonsContainer.Add(oldButton); + hiddenButtonsContainer.Add(oldButton); if (buttons.Count > 0) makeButtonDisappearToRight(oldButton, i, oldButtons.Length, true); @@ -188,7 +209,7 @@ namespace osu.Game.Screens.Footer } private ShearedOverlayContainer? activeOverlay; - private Container? contentContainer; + private VisibilityContainer? activeFooterContent; private readonly List temporarilyHiddenButtons = new List(); @@ -210,33 +231,28 @@ namespace osu.Game.Screens.Footer ? buttonsFlow.SkipWhile(b => b != targetButton).Skip(1) : buttonsFlow); - for (int i = 0; i < temporarilyHiddenButtons.Count; i++) - makeButtonDisappearToBottom(temporarilyHiddenButtons[i], 0, 0, false); + for (int i = temporarilyHiddenButtons.Count - 1; i >= 0; i--) + { + var button = temporarilyHiddenButtons[i]; + buttonsFlow.Remove(button, false); + hiddenButtonsContainer.Add(button); - var fallbackPosition = buttonsFlow.Any() - ? buttonsFlow.ToSpaceOfOtherDrawable(Vector2.Zero, this) - : BackButton.ToSpaceOfOtherDrawable(BackButton.LayoutRectangle.TopRight + new Vector2(5f, 0f), this); - - var targetPosition = targetButton?.ToSpaceOfOtherDrawable(targetButton.LayoutRectangle.TopRight, this) ?? fallbackPosition; + makeButtonDisappearToBottom(button, 0, 0, false); + } updateColourScheme(overlay.ColourProvider.Hue); footerContent = overlay.CreateFooterContent(); + activeFooterContent = footerContent; + var content = footerContent; - var content = footerContent ?? Empty(); - - Add(contentContainer = new Container - { - Y = -15f, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = targetPosition.X }, - Child = content, - }); + if (content != null) + footerContentContainer.Child = content; if (temporarilyHiddenButtons.Count > 0) - this.Delay(60).Schedule(() => content.Show()); + this.Delay(60).Schedule(() => content?.Show()); else - content.Show(); + content?.Show(); return new InvokeOnDisposal(clearActiveOverlayContainer); } @@ -246,20 +262,26 @@ namespace osu.Game.Screens.Footer if (activeOverlay == null) return; - Debug.Assert(contentContainer != null); - contentContainer.Child.Hide(); + Debug.Assert(activeFooterContent != null); + activeFooterContent.Hide(); - double timeUntilRun = contentContainer.Child.LatestTransformEndTime - Time.Current; + double timeUntilRun = activeFooterContent.LatestTransformEndTime - Time.Current; for (int i = 0; i < temporarilyHiddenButtons.Count; i++) - makeButtonAppearFromBottom(temporarilyHiddenButtons[i], 0); + { + var button = temporarilyHiddenButtons[i]; + hiddenButtonsContainer.Remove(button, false); + buttonsFlow.Add(button); + + makeButtonAppearFromBottom(button, 0); + } temporarilyHiddenButtons.Clear(); updateColourScheme(OverlayColourScheme.Aquamarine.GetHue()); - contentContainer.Delay(timeUntilRun).Expire(); - contentContainer = null; + activeFooterContent.Delay(timeUntilRun).Expire(); + activeFooterContent = null; activeOverlay = null; } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 135b3dba17..7d792a6bb8 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -20,6 +20,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -45,7 +46,7 @@ using osu.Game.Localisation; namespace osu.Game.Screens.Menu { - public partial class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler + public partial class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler, ISamplePlaybackDisabler { public const float FADE_IN_DURATION = 300; @@ -84,6 +85,10 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private IDialogOverlay dialogOverlay { get; set; } + // used to stop kiai fountain samples when navigating to other screens + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; + private readonly Bindable samplePlaybackDisabled = new Bindable(); + protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); protected override bool PlayExitSound => false; @@ -369,6 +374,8 @@ namespace osu.Game.Screens.Menu supporterDisplay .FadeOut(500, Easing.OutQuint); + + samplePlaybackDisabled.Value = true; } public override void OnResuming(ScreenTransitionEvent e) @@ -389,6 +396,8 @@ namespace osu.Game.Screens.Menu bottomElementsFlow .ScaleTo(1, 1000, Easing.OutQuint) .FadeIn(1000, Easing.OutQuint); + + samplePlaybackDisabled.Value = false; } public override bool OnExiting(ScreenExitEvent e) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs index 6de11ec34c..5c8b500c93 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallenge.cs @@ -39,6 +39,7 @@ using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; +using osu.Game.Users; using osuTK; namespace osu.Game.Screens.OnlinePlay.DailyChallenge @@ -107,6 +108,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge public override bool? ApplyModTrackAdjustments => true; + protected override UserActivity InitialActivity => new UserActivity.InDailyChallengeLobby(); + public DailyChallenge(Room room) { this.room = room; @@ -526,7 +529,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge private void startPlay() { sampleStart?.Play(); - this.Push(new PlayerLoader(() => new PlaylistsPlayer(room, playlistItem) + this.Push(new PlayerLoader(() => new DailyChallengePlayer(room, playlistItem) { Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores()) })); diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs new file mode 100644 index 0000000000..a5c61b8386 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/DailyChallengePlayer.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Playlists; +using osu.Game.Screens.Play; +using osu.Game.Users; + +namespace osu.Game.Screens.OnlinePlay.DailyChallenge +{ + public partial class DailyChallengePlayer : PlaylistsPlayer + { + protected override UserActivity InitialActivity => new UserActivity.PlayingDailyChallenge(Beatmap.Value.BeatmapInfo, Ruleset.Value); + + public DailyChallengePlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration? configuration = null) + : base(room, playlistItem, configuration) + { + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs index 0d90d44496..a91b844900 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Threading; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.Countdown; -using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osuTK; @@ -23,22 +22,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public partial class MatchStartControl : CompositeDrawable { - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; - [Resolved(canBeNull: true)] + [Resolved] private IDialogOverlay? dialogOverlay { get; set; } [Resolved] private MultiplayerClient client { get; set; } = null!; - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private readonly MultiplayerReadyButton readyButton; private readonly MultiplayerCountdownButton countdownButton; @@ -98,9 +90,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => updateState()); client.RoomUpdated += onRoomUpdated; client.LoadRequested += onLoadRequested; + updateState(); } @@ -214,8 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match readyButton.Enabled.Value = countdownButton.Enabled.Value = client.Room.State != MultiplayerRoomState.Closed - && SelectedItem.Value?.ID == client.Room.Settings.PlaylistItemId - && !client.Room.Playlist.Single(i => i.ID == client.Room.Settings.PlaylistItemId).Expired + && !client.Room.CurrentPlaylistItem.Expired && !operationInProgress.Value; // When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index 2b592bd8b9..b3923ddde3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -1,10 +1,8 @@ // 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.Framework.Graphics.Containers; -using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { @@ -13,14 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private const float ready_button_width = 600; private const float spectate_button_width = 200; - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); - public MultiplayerMatchFooter() { RelativeSizeAxes = Axes.Both; @@ -36,13 +26,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match new MultiplayerSpectateButton { RelativeSizeAxes = Axes.Both, - SelectedItem = selectedItem }, null, new MatchStartControl { RelativeSizeAxes = Axes.Both, - SelectedItem = selectedItem }, null } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 3186cf89a4..13abe7bb14 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -21,12 +21,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public partial class MultiplayerSpectateButton : CompositeDrawable { - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; @@ -36,7 +30,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private MultiplayerClient client { get; set; } = null!; - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private readonly RoundedButton button; private IBindable operationInProgress = null!; @@ -75,7 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true); client.RoomUpdated += onRoomUpdated; updateState(); } @@ -121,11 +113,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void checkForAutomaticDownload() { - PlaylistItem? item = SelectedItem.Value; - downloadCheckCancellation?.Cancel(); - if (item == null) + if (client.Room == null) return; if (!automaticallyDownload.Value) @@ -140,10 +130,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (client.LocalUser?.State != MultiplayerUserState.Spectating) return; + MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; + // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache - .GetBeatmapAsync(item.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token) + .GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token) .ContinueWith(resolved => Schedule(() => { var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs index 0c04c2712c..846f781cdc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchFreestyleSelect.cs @@ -60,6 +60,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return false; } + if (!base.OnStart()) + return false; + selectionOperation = operationTracker.BeginOperation(); client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 5a2da5a555..2b3243e01d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -254,10 +254,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer this.Push(new MultiplayerMatchFreestyleSelect(Room, item)); } - protected override Drawable CreateFooter() => new MultiplayerMatchFooter - { - SelectedItem = SelectedItem - }; + protected override Drawable CreateFooter() => new MultiplayerMatchFooter(); protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs index 4844d096ce..66218c0e9e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs @@ -7,6 +7,7 @@ using Humanizer; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.Rooms; @@ -43,6 +44,40 @@ namespace osu.Game.Screens.OnlinePlay LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; } + protected override bool OnStart() + { + FilterCriteria criteria = FilterControl.CreateCriteria(); + + // Beatmaps with too different of a duration are filtered away; this is just a final safety. + if (!criteria.Length.IsInRange(Beatmap.Value.BeatmapInfo.Length)) + { + Logger.Log("The selected beatmap's duration differs too much from the host's selection.", level: LogLevel.Error); + return false; + } + + // Beatmaps without a valid online ID are filtered away; this is just a final safety. + if (Beatmap.Value.BeatmapInfo.OnlineID < 0) + { + Logger.Log("The selected beatmap is not available online.", level: LogLevel.Error); + return false; + } + + // Beatmaps from different sets are filtered away; this is just a final safety. + if (Beatmap.Value.BeatmapSetInfo.OnlineID != criteria.BeatmapSetId) + { + Logger.Log("The selected beatmap is from a different beatmap set.", level: LogLevel.Error); + return false; + } + + if (Ruleset.Value.OnlineID < 0) + { + Logger.Log("The selected ruleset is not available online.", level: LogLevel.Error); + return false; + } + + return true; + } + protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item); protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs index 9c85088cc9..1f0f92aea2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFreestyleSelect.cs @@ -21,15 +21,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override bool OnStart() { - // Beatmaps without a valid online ID are filtered away; this is just a final safety. - if (base.Beatmap.Value.BeatmapInfo.OnlineID < 0) - return false; - - if (base.Ruleset.Value.OnlineID < 0) + if (!base.OnStart()) return false; Beatmap.Value = base.Beatmap.Value.BeatmapInfo; Ruleset.Value = base.Ruleset.Value; + this.Exit(); return true; } diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 3ab4c15154..986bc525cc 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -67,6 +67,12 @@ namespace osu.Game.Screens.Play.HUD } } + public FillDirection FillDirection + { + get => iconsContainer.Direction; + set => iconsContainer.Direction = value; + } + private readonly FillFlowContainer iconsContainer; public ModDisplay(bool showExtendedInformation = true) @@ -122,13 +128,13 @@ namespace osu.Game.Screens.Play.HUD private void expand(double duration = 500) { if (ExpansionMode != ExpansionMode.AlwaysContracted) - iconsContainer.TransformSpacingTo(new Vector2(5, 0), duration, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(5, -10), duration, Easing.OutQuint); } private void contract(double duration = 500) { if (ExpansionMode != ExpansionMode.AlwaysExpanded) - iconsContainer.TransformSpacingTo(new Vector2(-25, 0), duration, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(-25), duration, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs index 59bb1ade41..29b8429539 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableModDisplay.cs @@ -30,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpansionMode), nameof(SkinnableModDisplayStrings.ExpansionModeDescription))] public Bindable ExpansionModeSetting { get; } = new Bindable(); + [SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.DisplayDirection))] + public Bindable Direction { get; } = new Bindable(); + [BackgroundDependencyLoader] private void load() { @@ -50,6 +53,7 @@ namespace osu.Game.Screens.Play.HUD ShowExtendedInformation.BindValueChanged(_ => modDisplay.ShowExtendedInformation = ShowExtendedInformation.Value, true); ExpansionModeSetting.BindValueChanged(_ => modDisplay.ExpansionMode = ExpansionModeSetting.Value, true); + Direction.BindValueChanged(_ => modDisplay.FillDirection = Direction.Value == Framework.Graphics.Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical, true); FinishTransforms(true); } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 78c602d8f1..75a28a4240 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -147,6 +147,9 @@ namespace osu.Game.Screens.Play Direction = FillDirection.Vertical, Children = new Drawable[] { + // This display is potentially a duplicate of users with a local ModDisplay in their skins. + // It would be very nice to remove this, but the version here has special logic with regards to replays + // and initial states, so needs a bit of thought before doing so. ModDisplay = CreateModsContainer(), } }, diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 3a471acba4..18d17c1317 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -5,9 +5,12 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Framework.Platform; using osu.Game.Audio; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -31,14 +34,29 @@ namespace osu.Game.Screens.Play OnResume?.Invoke(); }; + private readonly IBindable windowActive = new Bindable(true); + + private float targetVolume => windowActive.Value && State.Value == Visibility.Visible ? 1.0f : 0; + [BackgroundDependencyLoader] - private void load() + private void load(GameHost? host) { AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("Gameplay/pause-loop")) { Looping = true, Volume = { Value = 0 } }); + + if (host != null) + windowActive.BindTo(host.IsActive); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Schedule required because host.IsActive doesn't seem to always run on the update thread. + windowActive.BindValueChanged(_ => Schedule(() => pauseLoop.VolumeTo(targetVolume, 1000, Easing.Out))); } public void StopAllSamples() @@ -53,7 +71,7 @@ namespace osu.Game.Screens.Play { base.PopIn(); - pauseLoop.VolumeTo(1.0f, TRANSITION_DURATION, Easing.InQuint); + pauseLoop.VolumeTo(targetVolume, TRANSITION_DURATION, Easing.InQuint); pauseLoop.Play(); } @@ -61,7 +79,7 @@ namespace osu.Game.Screens.Play { base.PopOut(); - pauseLoop.VolumeTo(0, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); + pauseLoop.VolumeTo(targetVolume, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); } public override bool OnPressed(KeyBindingPressEvent e) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index e9d0bf3403..fbc0fd8a70 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -108,12 +108,10 @@ namespace osu.Game.Screens.Ranking.Contracted Offset = new Vector2(0, 1), } }, - new OsuSpriteText + new ClickableUsername(score.User) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = score.RealmUser.Username, - Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold) }, new FillFlowContainer { diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 0190a6f959..445d219c7f 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -106,22 +106,7 @@ namespace osu.Game.Screens.Ranking.Expanded Direction = FillDirection.Vertical, Children = new Drawable[] { - new TruncatingSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), - MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - }, - new TruncatingSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), - MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - }, + new ClickableMetadata(beatmap.OnlineID, metadata), new Container { Anchor = Anchor.TopCentre, @@ -316,5 +301,47 @@ namespace osu.Game.Screens.Ranking.Expanded time.ToLocalTime().ToLocalisableString(prefer24HourTime.Value ? @"d MMMM yyyy HH:mm" : @"d MMMM yyyy h:mm tt")); } } + + internal partial class ClickableMetadata : OsuHoverContainer + { + [Resolved] + private OsuGame? game { get; set; } + + public ClickableMetadata(int beatmapId, IBeatmapMetadataInfo metadata) + { + AutoSizeAxes = Axes.Both; + + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TruncatingSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), + MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, + }, + new TruncatingSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, + } + } + }; + + if (beatmapId > 0) + Action = () => game?.ShowBeatmap(beatmapId); + } + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs index c834d541eb..b50996154b 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs @@ -8,8 +8,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users.Drawables; using osuTK; @@ -62,12 +60,10 @@ namespace osu.Game.Screens.Ranking.Expanded CornerExponent = 2.5f, Masking = true, }, - new OsuSpriteText + new ClickableUsername(user) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = user.Username, - Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold) } } }; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 758eabcf2e..ad868e58f0 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -14,14 +14,18 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Models; using osu.Game.Online.API; using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics.User; using osuTK; +using Realms; namespace osu.Game.Screens.Ranking.Statistics { @@ -43,6 +47,9 @@ namespace osu.Game.Screens.Ranking.Statistics [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; + [Resolved] + private RealmAccess realm { get; set; } = null!; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -231,14 +238,32 @@ namespace osu.Game.Screens.Ranking.Statistics }); } - if (AchievedScore != null - && newScore.BeatmapInfo!.OnlineID > 0 + if (newScore.BeatmapInfo!.OnlineID > 0 && api.IsLoggedIn) { - if ( - // We may want to iterate on this condition - AchievedScore.Rank >= ScoreRank.C - ) + string? preventTaggingReason = null; + + // We may want to iterate on the following conditions further in the future + + var localUserScore = AchievedScore ?? realm.Run(r => + r.All() + .Filter($@"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $@" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $@" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, newScore.BeatmapInfo.ID, newScore.BeatmapInfo.Ruleset.ShortName) + .AsEnumerable() + .OrderByDescending(score => score.Ruleset.MatchesOnlineID(newScore.BeatmapInfo.Ruleset)) + .ThenByDescending(score => score.Rank) + .FirstOrDefault()); + + if (localUserScore == null) + preventTaggingReason = "Play the beatmap to contribute to beatmap tags!"; + else if (localUserScore.Ruleset.OnlineID != newScore.BeatmapInfo!.Ruleset.OnlineID) + preventTaggingReason = "Play the beatmap in its original ruleset to contribute to beatmap tags!"; + else if (localUserScore.Rank < ScoreRank.C) + preventTaggingReason = "Set a better score to contribute to beatmap tags!"; + + if (preventTaggingReason == null) { yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo) { @@ -254,7 +279,7 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, TextAnchor = Anchor.Centre, - Text = "Set a better score to contribute to beatmap tags!", + Text = preventTaggingReason, }); } } diff --git a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs index 0d91d6f8f9..5ffea094cd 100644 --- a/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/User/GlobalRankChangeRow.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; -using osu.Game.Utils; namespace osu.Game.Screens.Ranking.Statistics.User { @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking.Statistics.User protected override LocalisableString Label => UsersStrings.ShowRankGlobalSimple; protected override LocalisableString FormatCurrentValue(int? current) - => current == null ? string.Empty : current.Value.FormatRank(); + => current?.ToLocalisableString(@"N0") ?? string.Empty; protected override int CalculateDifference(int? previous, int? current, out LocalisableString formattedDifference) { @@ -30,13 +30,13 @@ namespace osu.Game.Screens.Ranking.Statistics.User if (previous == null && current != null) { - formattedDifference = LocalisableString.Interpolate($"+{current.Value.FormatRank()}"); + formattedDifference = LocalisableString.Interpolate($"+{current.Value:N0}"); return 1; } if (previous != null && current == null) { - formattedDifference = LocalisableString.Interpolate($"-{previous.Value.FormatRank()}"); + formattedDifference = LocalisableString.Interpolate($"-{previous.Value:N0}"); return -1; } @@ -46,9 +46,9 @@ namespace osu.Game.Screens.Ranking.Statistics.User int difference = previous.Value - current.Value; if (difference < 0) - formattedDifference = difference.FormatRank(); + formattedDifference = difference.ToLocalisableString(@"N0"); else if (difference > 0) - formattedDifference = LocalisableString.Interpolate($"+{difference.FormatRank()}"); + formattedDifference = LocalisableString.Interpolate($"+{difference:N0}"); else formattedDifference = string.Empty; diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 1c1f6fa7fb..994b0fb6c0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.SelectV2 { public Action? RequestPresentBeatmap { private get; init; } - public const float SPACING = 5f; + public const float SPACING = 3f; private IBindableList detachedBeatmaps = null!; diff --git a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs index 0992203dbc..869aef1470 100644 --- a/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/Footer/ScreenFooterButtonMods.cs @@ -244,18 +244,18 @@ namespace osu.Game.Screens.SelectV2.Footer Mods.BindValueChanged(v => Text = ModSelectOverlayStrings.Mods(v.NewValue.Count).ToUpper(), true); } - public ITooltip> GetCustomTooltip() => new ModTooltip(colourProvider); + public ITooltip> GetCustomTooltip() => new ModOverflowTooltip(colourProvider); public IReadOnlyList? TooltipContent => Mods.Value; - public partial class ModTooltip : VisibilityContainer, ITooltip> + public partial class ModOverflowTooltip : VisibilityContainer, ITooltip> { private ModDisplay extendedModDisplay = null!; [Cached] private OverlayColourProvider colourProvider; - public ModTooltip(OverlayColourProvider colourProvider) + public ModOverflowTooltip(OverlayColourProvider colourProvider) { this.colourProvider = colourProvider; } diff --git a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs index b54f007f38..16599a2080 100644 --- a/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs +++ b/osu.Game/Screens/SelectV2/Leaderboards/LeaderboardScoreV2.cs @@ -715,18 +715,21 @@ namespace osu.Game.Screens.SelectV2.Leaderboards public LocalisableString TooltipText { get; } } - private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasTooltip + private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasCustomTooltip { - private readonly IMod mod; + public Mod? TooltipContent { get; } - public ColouredModSwitchTiny(IMod mod) + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public ColouredModSwitchTiny(Mod mod) : base(mod) { - this.mod = mod; + TooltipContent = mod; Active.Value = true; } - public LocalisableString TooltipText => (mod as Mod)?.IconTooltip ?? mod.Name; + public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); } private sealed partial class MoreModSwitchTiny : CompositeDrawable diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 512fbacec1..c599c3e534 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -106,8 +106,6 @@ namespace osu.Game.Screens.SelectV2 }, difficultiesDisplay = new DifficultySpectrumDisplay { - DotSize = new Vector2(5, 10), - DotSpacing = 2, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index ad29f846c4..e295656a21 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.SelectV2 { private const float logo_scale = 0.4f; - private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay + private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay(OverlayColourScheme.Aquamarine) { ShowPresets = true, }; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index cc9a82c1ba..febd7f54ff 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -238,7 +238,7 @@ namespace osu.Game.Tests.Visual.Multiplayer QueueMode = ServerAPIRoom.QueueMode, AutoStartDuration = ServerAPIRoom.AutoStartDuration }, - Playlist = ServerAPIRoom.Playlist.Select(CreateMultiplayerPlaylistItem).ToList(), + Playlist = ServerAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)).ToList(), Users = { localUser }, Host = localUser }; @@ -687,21 +687,6 @@ namespace osu.Game.Tests.Visual.Multiplayer return MessagePackSerializer.Deserialize(serialized, SignalRUnionWorkaroundResolver.OPTIONS); } - public static MultiplayerPlaylistItem CreateMultiplayerPlaylistItem(PlaylistItem item) => new MultiplayerPlaylistItem - { - ID = item.ID, - OwnerID = item.OwnerID, - BeatmapID = item.Beatmap.OnlineID, - BeatmapChecksum = item.Beatmap.MD5Hash, - RulesetID = item.RulesetID, - RequiredMods = item.RequiredMods.ToArray(), - AllowedMods = item.AllowedMods.ToArray(), - Expired = item.Expired, - PlaylistOrder = item.PlaylistOrder ?? 0, - PlayedAt = item.PlayedAt, - StarRating = item.Beatmap.StarRating, - }; - public override Task DisconnectInternal() { isConnected.Value = false; diff --git a/osu.Game/Users/Drawables/ClickableUsername.cs b/osu.Game/Users/Drawables/ClickableUsername.cs new file mode 100644 index 0000000000..74782ed6ed --- /dev/null +++ b/osu.Game/Users/Drawables/ClickableUsername.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Users.Drawables +{ + internal partial class ClickableUsername : OsuHoverContainer, IHasCustomTooltip + { + public ITooltip GetCustomTooltip() => new ClickableAvatar.NoCardTooltip(); + + public APIUser? TooltipContent { get; } + + private readonly APIUser user; + + [Resolved] + private OsuGame? game { get; set; } + + public ClickableUsername(APIUser? user) + { + TooltipContent = this.user = user ?? new GuestUser(); + + AutoSizeAxes = Axes.Both; + + Child = new OsuSpriteText + { + Text = user!.Username, + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + }; + + if (user.Id != APIUser.SYSTEM_USER_ID) + Action = openProfile; + } + + private void openProfile() + { + if (user.Id > 1 || !string.IsNullOrEmpty(user.Username)) + game?.ShowUser(user); + } + } +} diff --git a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs index 2fcec66aa7..517eb589b9 100644 --- a/osu.Game/Users/Drawables/UpdateableTeamFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableTeamFlag.cs @@ -8,8 +8,10 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Users.Drawables @@ -64,6 +66,12 @@ namespace osu.Game.Users.Drawables public LocalisableString TooltipText { get; } + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + public TeamFlag(APITeam team) { this.team = team; @@ -91,6 +99,12 @@ namespace osu.Game.Users.Drawables } }; } + + protected override bool OnClick(ClickEvent e) + { + game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/teams/{team.Id}"); + return true; + } } } } diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index a792424562..16b30546de 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -34,6 +34,8 @@ namespace osu.Game.Users [Union(41, typeof(EditingBeatmap))] [Union(42, typeof(ModdingBeatmap))] [Union(43, typeof(TestingBeatmap))] + [Union(51, typeof(InDailyChallengeLobby))] + [Union(52, typeof(PlayingDailyChallenge))] public abstract class UserActivity { public abstract string GetStatus(bool hideIdentifiableInformation = false); @@ -58,6 +60,7 @@ namespace osu.Game.Users [Union(23, typeof(InMultiplayerGame))] [Union(24, typeof(SpectatingMultiplayerGame))] [Union(31, typeof(InPlaylistGame))] + [Union(52, typeof(PlayingDailyChallenge))] public abstract class InGame : UserActivity { [Key(0)] @@ -244,7 +247,7 @@ namespace osu.Game.Users [SerializationConstructor] public SpectatingMultiplayerGame() { } - public override string GetStatus(bool hideIdentifiableInformation = false) => $"Watching others {base.GetStatus(hideIdentifiableInformation).ToLowerInvariant()}"; + public override string GetStatus(bool hideIdentifiableInformation = false) => @"Spectating a multiplayer game"; } [MessagePackObject] @@ -277,5 +280,30 @@ namespace osu.Game.Users ? null : RoomName; } + + [MessagePackObject] + public class InDailyChallengeLobby : UserActivity + { + [SerializationConstructor] + public InDailyChallengeLobby() { } + + public override string GetStatus(bool hideIdentifiableInformation = false) => @"In daily challenge lobby"; + } + + [MessagePackObject] + public class PlayingDailyChallenge : InGame + { + public PlayingDailyChallenge(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset) + : base(beatmapInfo, ruleset) + { + } + + [SerializationConstructor] + public PlayingDailyChallenge() + { + } + + public override string GetStatus(bool hideIdentifiableInformation = false) => @$"{RulesetPlayingVerb} in daily challenge"; + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index af052ae93b..e9d7bacc64 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,8 +35,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/osu.iOS.props b/osu.iOS.props index 260b0cc0c3..2fa83c3ab0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - +