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 = "",
+ });
+ 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 = "",
+ });
+ 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 = "",
+ });
+ 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
-
+