diff --git a/README.md b/README.md
index 24b70b2de6..f18c5e76f9 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir
Please make sure you have the following prerequisites:
-- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed.
+- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) installed.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
diff --git a/osu.Android.props b/osu.Android.props
index 209b8cd63e..ca4d88a8a7 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
index dbfd170ea1..4acaf61cea 100644
--- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
+++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
@@ -14,6 +14,7 @@ namespace osu.Desktop.Windows
{
private Bindable disableWinKey;
private IBindable localUserPlaying;
+ private IBindable isActive;
[Resolved]
private GameHost host { get; set; }
@@ -24,13 +25,16 @@ namespace osu.Desktop.Windows
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking());
+ isActive = host.IsActive.GetBoundCopy();
+ isActive.BindValueChanged(_ => updateBlocking());
+
disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
}
private void updateBlocking()
{
- bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
+ bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value;
if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable);
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
index 44ba0e2057..03abba29ce 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
}
- private double skillMultiplier => 0.15;
+ private double skillMultiplier => 0.07;
private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
@@ -40,26 +40,31 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double result = 0.0;
+ OsuDifficultyHitObject lastObj = osuCurrent;
+
+ // This is iterating backwards in time from the current object.
for (int i = 0; i < Previous.Count; i++)
{
- var osuPrevious = (OsuDifficultyHitObject)Previous[i];
- var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject);
+ var currentObj = (OsuDifficultyHitObject)Previous[i];
+ var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
- if (!(osuPrevious.BaseObject is Spinner))
+ if (!(currentObj.BaseObject is Spinner))
{
- double jumpDistance = (osuHitObject.StackedPosition - osuPreviousHitObject.EndPosition).Length;
+ double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
- cumulativeStrainTime += osuPrevious.StrainTime;
+ cumulativeStrainTime += lastObj.StrainTime;
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
if (i == 0)
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
// We also want to nerf stacks so that only the first object of the stack is accounted for.
- double stackNerf = Math.Min(1.0, (osuPrevious.LazyJumpDistance / scalingFactor) / 25.0);
+ double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
- result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
+ result += stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
}
+
+ lastObj = currentObj;
}
return Math.Pow(smallDistNerf * result, 2.0);
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index 8a063b3c6e..8d15be44fa 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -61,6 +61,7 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
+ cleanupPath(customPath);
}
}
}
@@ -94,6 +95,7 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
+ cleanupPath(customPath);
}
}
}
@@ -160,6 +162,7 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
+ cleanupPath(customPath);
}
}
}
@@ -168,7 +171,7 @@ namespace osu.Game.Tests.NonVisual
public void TestMigrationBetweenTwoTargets()
{
string customPath = prepareCustomPath();
- string customPath2 = prepareCustomPath("-2");
+ string customPath2 = prepareCustomPath();
using (var host = new CustomTestHeadlessGameHost())
{
@@ -185,7 +188,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
// some files may have been left behind for whatever reason, but that's not what we're testing here.
- customPath = prepareCustomPath();
+ cleanupPath(customPath);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
@@ -193,6 +196,8 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
+ cleanupPath(customPath);
+ cleanupPath(customPath2);
}
}
}
@@ -214,6 +219,7 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
+ cleanupPath(customPath);
}
}
}
@@ -243,6 +249,7 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
+ cleanupPath(customPath);
}
}
}
@@ -272,6 +279,7 @@ namespace osu.Game.Tests.NonVisual
finally
{
host.Exit();
+ cleanupPath(customPath);
}
}
}
@@ -286,14 +294,18 @@ namespace osu.Game.Tests.NonVisual
return path;
}
- private string prepareCustomPath(string suffix = "")
+ private static string prepareCustomPath() => Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}");
+
+ private static void cleanupPath(string path)
{
- string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path{suffix}");
-
- if (Directory.Exists(path))
- Directory.Delete(path, true);
-
- return path;
+ try
+ {
+ if (Directory.Exists(path))
+ Directory.Delete(path, true);
+ }
+ catch
+ {
+ }
}
public class CustomTestHeadlessGameHost : CleanRunHeadlessGameHost
diff --git a/osu.Game.Tests/NonVisual/SessionStaticsTest.cs b/osu.Game.Tests/NonVisual/SessionStaticsTest.cs
index d5fd803986..cd02f15adf 100644
--- a/osu.Game.Tests/NonVisual/SessionStaticsTest.cs
+++ b/osu.Game.Tests/NonVisual/SessionStaticsTest.cs
@@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using NUnit.Framework;
using osu.Game.Configuration;
-using osu.Game.Input;
+using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tests.NonVisual
{
@@ -11,37 +12,32 @@ namespace osu.Game.Tests.NonVisual
public class SessionStaticsTest
{
private SessionStatics sessionStatics;
- private IdleTracker sessionIdleTracker;
- [SetUp]
- public void SetUp()
+ [Test]
+ public void TestSessionStaticsReset()
{
sessionStatics = new SessionStatics();
- sessionIdleTracker = new GameIdleTracker(1000);
sessionStatics.SetValue(Static.LoginOverlayDisplayed, true);
sessionStatics.SetValue(Static.MutedAudioNotificationShownOnce, true);
sessionStatics.SetValue(Static.LowBatteryNotificationShownOnce, true);
sessionStatics.SetValue(Static.LastHoverSoundPlaybackTime, (double?)1d);
+ sessionStatics.SetValue(Static.SeasonalBackgrounds, new APISeasonalBackgrounds { EndDate = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero) });
- sessionIdleTracker.IsIdle.BindValueChanged(e =>
- {
- if (e.NewValue)
- sessionStatics.ResetValues();
- });
- }
+ Assert.IsFalse(sessionStatics.GetBindable(Static.LoginOverlayDisplayed).IsDefault);
+ Assert.IsFalse(sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).IsDefault);
+ Assert.IsFalse(sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce).IsDefault);
+ Assert.IsFalse(sessionStatics.GetBindable(Static.LastHoverSoundPlaybackTime).IsDefault);
+ Assert.IsFalse(sessionStatics.GetBindable(Static.SeasonalBackgrounds).IsDefault);
- [Test]
- [Timeout(2000)]
- public void TestSessionStaticsReset()
- {
- sessionIdleTracker.IsIdle.BindValueChanged(e =>
- {
- Assert.IsTrue(sessionStatics.GetBindable(Static.LoginOverlayDisplayed).IsDefault);
- Assert.IsTrue(sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).IsDefault);
- Assert.IsTrue(sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce).IsDefault);
- Assert.IsTrue(sessionStatics.GetBindable(Static.LastHoverSoundPlaybackTime).IsDefault);
- });
+ sessionStatics.ResetAfterInactivity();
+
+ Assert.IsTrue(sessionStatics.GetBindable(Static.LoginOverlayDisplayed).IsDefault);
+ Assert.IsTrue(sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).IsDefault);
+ Assert.IsTrue(sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce).IsDefault);
+ // some statics should not reset despite inactivity.
+ Assert.IsFalse(sessionStatics.GetBindable(Static.LastHoverSoundPlaybackTime).IsDefault);
+ Assert.IsFalse(sessionStatics.GetBindable(Static.SeasonalBackgrounds).IsDefault);
}
}
}
diff --git a/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs
new file mode 100644
index 0000000000..d33081662d
--- /dev/null
+++ b/osu.Game.Tests/OnlinePlay/PlaylistExtensionsTest.cs
@@ -0,0 +1,100 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Game.Online.Rooms;
+
+namespace osu.Game.Tests.OnlinePlay
+{
+ [TestFixture]
+ public class PlaylistExtensionsTest
+ {
+ [Test]
+ public void TestEmpty()
+ {
+ // mostly an extreme edge case, i.e. during room creation.
+ var items = Array.Empty();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(items.GetHistoricalItems(), Is.Empty);
+ Assert.That(items.GetCurrentItem(), Is.Null);
+ Assert.That(items.GetUpcomingItems(), Is.Empty);
+ });
+ }
+
+ [Test]
+ public void TestPlaylistItemsInOrder()
+ {
+ var items = new[]
+ {
+ new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 },
+ new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 },
+ new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 },
+ };
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(items.GetHistoricalItems(), Is.Empty);
+ Assert.That(items.GetCurrentItem(), Is.EqualTo(items[0]));
+ Assert.That(items.GetUpcomingItems(), Is.EquivalentTo(items));
+ });
+ }
+
+ [Test]
+ public void TestPlaylistItemsOutOfOrder()
+ {
+ var items = new[]
+ {
+ new PlaylistItem { ID = 2, BeatmapID = 1002, PlaylistOrder = 2 },
+ new PlaylistItem { ID = 1, BeatmapID = 1001, PlaylistOrder = 1 },
+ new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 },
+ };
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(items.GetHistoricalItems(), Is.Empty);
+ Assert.That(items.GetCurrentItem(), Is.EqualTo(items[1]));
+ Assert.That(items.GetUpcomingItems(), Is.EquivalentTo(new[] { items[1], items[0], items[2] }));
+ });
+ }
+
+ [Test]
+ public void TestExpiredPlaylistItemsSkipped()
+ {
+ var items = new[]
+ {
+ new PlaylistItem { ID = 1, BeatmapID = 1001, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) },
+ new PlaylistItem { ID = 2, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) },
+ new PlaylistItem { ID = 3, BeatmapID = 1003, PlaylistOrder = 3 },
+ };
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(items.GetHistoricalItems(), Is.EquivalentTo(new[] { items[1], items[0] }));
+ Assert.That(items.GetCurrentItem(), Is.EqualTo(items[2]));
+ Assert.That(items.GetUpcomingItems(), Is.EquivalentTo(new[] { items[2] }));
+ });
+ }
+
+ [Test]
+ public void TestAllItemsExpired()
+ {
+ var items = new[]
+ {
+ new PlaylistItem { ID = 1, BeatmapID = 1001, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 55, 0, TimeSpan.Zero) },
+ new PlaylistItem { ID = 2, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 53, 0, TimeSpan.Zero) },
+ new PlaylistItem { ID = 3, BeatmapID = 1002, Expired = true, PlayedAt = new DateTimeOffset(2021, 12, 21, 7, 57, 0, TimeSpan.Zero) },
+ };
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(items.GetHistoricalItems(), Is.EquivalentTo(new[] { items[1], items[0], items[2] }));
+ // if all items are expired, the last-played item is expected to be returned.
+ Assert.That(items.GetCurrentItem(), Is.EqualTo(items[2]));
+ Assert.That(items.GetUpcomingItems(), Is.Empty);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index 3211405670..844fe7705a 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -85,6 +85,7 @@ namespace osu.Game.Tests.Visual.Background
// of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack.
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
+ AddUntilStep("previous background hidden", () => !screen.IsAlive);
AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
index 0b9857486a..7b5e1f4ec7 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs
@@ -96,6 +96,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
var longName = CreateAPIBeatmapSet(Ruleset.Value);
longName.Title = longName.TitleUnicode = "this track has an incredibly and implausibly long title";
longName.Artist = longName.ArtistUnicode = "and this artist! who would have thunk it. it's really such a long name.";
+ longName.Source = "wow. even the source field has an impossibly long string in it. this really takes the cake, doesn't it?";
longName.HasExplicitContent = true;
longName.TrackId = 444;
@@ -251,13 +252,19 @@ namespace osu.Game.Tests.Visual.Beatmaps
[Test]
public void TestNormal()
{
- createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
+ createTestCase(beatmapSetInfo => new BeatmapCardNormal(beatmapSetInfo));
+ }
+
+ [Test]
+ public void TestExtra()
+ {
+ createTestCase(beatmapSetInfo => new BeatmapCardExtra(beatmapSetInfo));
}
[Test]
public void TestHoverState()
{
- AddStep("create cards", () => Child = createContent(OverlayColourScheme.Blue, s => new BeatmapCard(s)));
+ AddStep("create cards", () => Child = createContent(OverlayColourScheme.Blue, s => new BeatmapCardNormal(s)));
AddStep("Hover card", () => InputManager.MoveMouseTo(firstCard()));
AddWaitStep("wait for potential state change", 5);
@@ -274,10 +281,10 @@ namespace osu.Game.Tests.Visual.Beatmaps
AddWaitStep("wait for potential state change", 5);
AddAssert("card is still expanded", () => firstCard().Expanded.Value);
- AddStep("Hover away", () => InputManager.MoveMouseTo(this.ChildrenOfType().Last()));
+ AddStep("Hover away", () => InputManager.MoveMouseTo(this.ChildrenOfType().Last()));
AddUntilStep("card is not expanded", () => !firstCard().Expanded.Value);
- BeatmapCard firstCard() => this.ChildrenOfType().First();
+ BeatmapCardNormal firstCard() => this.ChildrenOfType().First();
}
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
index 357db16e2c..88c54eb2bb 100644
--- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs
@@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -20,7 +19,6 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play;
using osu.Game.Tests.Resources;
-using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -31,17 +29,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected BeatmapInfo InitialBeatmap { get; private set; }
protected BeatmapInfo OtherBeatmap { get; private set; }
- protected IScreen CurrentScreen => multiplayerScreenStack.CurrentScreen;
- protected IScreen CurrentSubScreen => multiplayerScreenStack.MultiplayerScreen.CurrentSubScreen;
+ protected IScreen CurrentScreen => multiplayerComponents.CurrentScreen;
+ protected IScreen CurrentSubScreen => multiplayerComponents.MultiplayerScreen.CurrentSubScreen;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
- private TestMultiplayerScreenStack multiplayerScreenStack;
+ private TestMultiplayerComponents multiplayerComponents;
- protected TestMultiplayerClient Client => multiplayerScreenStack.Client;
- protected TestMultiplayerRoomManager RoomManager => multiplayerScreenStack.RoomManager;
+ protected TestMultiplayerClient Client => multiplayerComponents.Client;
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
@@ -65,12 +62,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0);
});
- AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack()));
- AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded);
+ AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
+ AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
- AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
- AddStep("open room", () => multiplayerScreenStack.ChildrenOfType().Single().Open(new Room
+ AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
+ AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open(new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = Mode },
@@ -87,13 +84,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
- AddStep("create room", () =>
- {
- InputManager.MoveMouseTo(this.ChildrenOfType().Single());
- InputManager.Click(MouseButton.Left);
- });
+ ClickButtonWhenEnabled();
- AddUntilStep("wait for join", () => RoomManager.RoomJoined);
+ AddUntilStep("wait for join", () => Client.RoomJoined);
}
[Test]
@@ -105,24 +98,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected void RunGameplay()
{
AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle);
- clickReadyButton();
+ ClickButtonWhenEnabled();
AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready);
- clickReadyButton();
+ ClickButtonWhenEnabled();
- AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player player && player.IsLoaded);
- AddStep("exit player", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent());
- }
-
- private void clickReadyButton()
- {
- AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType().Single().ChildrenOfType
///
- public class TestMultiplayerScreenStack : OsuScreen
+ public class TestMultiplayerComponents : OsuScreen
{
public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen;
@@ -42,14 +42,18 @@ namespace osu.Game.Tests.Visual
private readonly OsuScreenStack screenStack;
private readonly TestMultiplayer multiplayerScreen;
- public TestMultiplayerScreenStack()
+ public TestMultiplayerComponents()
{
multiplayerScreen = new TestMultiplayer();
InternalChildren = new Drawable[]
{
Client = new TestMultiplayerClient(RoomManager),
- screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }
+ screenStack = new OsuScreenStack
+ {
+ Name = nameof(TestMultiplayerComponents),
+ RelativeSizeAxes = Axes.Both
+ }
};
screenStack.Push(multiplayerScreen);
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
index 1e24501426..6adcdcfcb2 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs
@@ -3,431 +3,78 @@
#nullable enable
-using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
-using osu.Framework.Localisation;
-using osu.Game.Beatmaps.Drawables.Cards.Buttons;
-using osu.Game.Beatmaps.Drawables.Cards.Statistics;
-using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
-using osu.Game.Overlays.BeatmapSet;
-using osuTK;
-using osu.Game.Resources.Localisation.Web;
-using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton;
namespace osu.Game.Beatmaps.Drawables.Cards
{
- public class BeatmapCard : OsuClickableContainer
+ public abstract class BeatmapCard : OsuClickableContainer
{
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
public IBindable Expanded { get; }
- private const float width = 408;
- private const float height = 100;
- private const float icon_area_width = 30;
+ protected readonly APIBeatmapSet BeatmapSet;
+ protected readonly Bindable FavouriteState;
- private readonly APIBeatmapSet beatmapSet;
- private readonly Bindable favouriteState;
+ protected abstract Drawable IdleContent { get; }
+ protected abstract Drawable DownloadInProgressContent { get; }
- private readonly BeatmapDownloadTracker downloadTracker;
+ protected readonly BeatmapDownloadTracker DownloadTracker;
- private BeatmapCardContent content = null!;
-
- private BeatmapCardThumbnail thumbnail = null!;
-
- private Container rightAreaBackground = null!;
- private Container rightAreaButtons = null!;
-
- private Container mainContent = null!;
- private BeatmapCardContentBackground mainContentBackground = null!;
- private FillFlowContainer statisticsContainer = null!;
-
- private FillFlowContainer idleBottomContent = null!;
- private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
-
- [Resolved]
- private OsuColour colours { get; set; } = null!;
-
- [Resolved]
- private OverlayColourProvider colourProvider { get; set; } = null!;
-
- public BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
+ protected BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
: base(HoverSampleSet.Submit)
{
Expanded = new BindableBool { Disabled = !allowExpansion };
- this.beatmapSet = beatmapSet;
- favouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
- downloadTracker = new BeatmapDownloadTracker(beatmapSet);
+ BeatmapSet = beatmapSet;
+ FavouriteState = new Bindable(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
+ DownloadTracker = new BeatmapDownloadTracker(beatmapSet);
}
[BackgroundDependencyLoader(true)]
private void load(BeatmapSetOverlay? beatmapSetOverlay)
{
- Width = width;
- Height = height;
+ Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
- FillFlowContainer leftIconArea;
- GridContainer titleContainer;
- GridContainer artistContainer;
-
- InternalChild = content = new BeatmapCardContent(height)
- {
- MainContent = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- downloadTracker,
- rightAreaBackground = new Container
- {
- RelativeSizeAxes = Axes.Y,
- Width = icon_area_width + 2 * CORNER_RADIUS,
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- // workaround for masking artifacts at the top & bottom of card,
- // which become especially visible on downloaded beatmaps (when the icon area has a lime background).
- Padding = new MarginPadding { Vertical = 1 },
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Colour4.White
- },
- },
- thumbnail = new BeatmapCardThumbnail(beatmapSet)
- {
- Name = @"Left (icon) area",
- Size = new Vector2(height),
- Padding = new MarginPadding { Right = CORNER_RADIUS },
- Child = leftIconArea = new FillFlowContainer
- {
- Margin = new MarginPadding(5),
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(1)
- }
- },
- new Container
- {
- Name = @"Right (button) area",
- Width = 30,
- RelativeSizeAxes = Axes.Y,
- Origin = Anchor.TopRight,
- Anchor = Anchor.TopRight,
- Padding = new MarginPadding { Vertical = 17.5f },
- Child = rightAreaButtons = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new BeatmapCardIconButton[]
- {
- new FavouriteButton(beatmapSet)
- {
- Current = favouriteState,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
- },
- new DownloadButton(beatmapSet)
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- State = { BindTarget = downloadTracker.State }
- },
- new GoToBeatmapButton(beatmapSet)
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- State = { BindTarget = downloadTracker.State }
- }
- }
- }
- },
- mainContent = new Container
- {
- Name = @"Main content",
- X = height - CORNER_RADIUS,
- Height = height,
- CornerRadius = CORNER_RADIUS,
- Masking = true,
- Children = new Drawable[]
- {
- mainContentBackground = new BeatmapCardContentBackground(beatmapSet)
- {
- RelativeSizeAxes = Axes.Both,
- },
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
- {
- Horizontal = 10,
- Vertical = 4
- },
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
- {
- titleContainer = new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.AutoSize)
- },
- RowDimensions = new[]
- {
- new Dimension(GridSizeMode.AutoSize)
- },
- Content = new[]
- {
- new[]
- {
- new OsuSpriteText
- {
- Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
- Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
- RelativeSizeAxes = Axes.X,
- Truncate = true
- },
- Empty()
- }
- }
- },
- artistContainer = new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
- {
- new Dimension(),
- new Dimension(GridSizeMode.AutoSize)
- },
- RowDimensions = new[]
- {
- new Dimension(GridSizeMode.AutoSize)
- },
- Content = new[]
- {
- new[]
- {
- new OsuSpriteText
- {
- Text = createArtistText(),
- Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
- RelativeSizeAxes = Axes.X,
- Truncate = true
- },
- Empty()
- },
- }
- },
- new LinkFlowContainer(s =>
- {
- s.Shadow = false;
- s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
- }).With(d =>
- {
- d.AutoSizeAxes = Axes.Both;
- d.Margin = new MarginPadding { Top = 2 };
- d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
- d.AddUserLink(beatmapSet.Author);
- }),
- }
- },
- new Container
- {
- Name = @"Bottom content",
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- Padding = new MarginPadding
- {
- Horizontal = 10,
- Vertical = 4
- },
- Children = new Drawable[]
- {
- idleBottomContent = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 3),
- AlwaysPresent = true,
- Children = new Drawable[]
- {
- statisticsContainer = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(10, 0),
- Alpha = 0,
- AlwaysPresent = true,
- ChildrenEnumerable = createStatistics()
- },
- new BeatmapCardExtraInfoRow(beatmapSet)
- {
- Hovered = _ =>
- {
- content.ExpandAfterDelay();
- return false;
- },
- Unhovered = _ =>
- {
- // Handles the case where a user has not shown explicit intent to view expanded info.
- // ie. quickly moved over the info row area but didn't remain within it.
- if (!Expanded.Value)
- content.CancelExpand();
- }
- }
- }
- },
- downloadProgressBar = new BeatmapCardDownloadProgressBar
- {
- RelativeSizeAxes = Axes.X,
- Height = 6,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- State = { BindTarget = downloadTracker.State },
- Progress = { BindTarget = downloadTracker.Progress }
- }
- }
- }
- }
- }
- }
- },
- ExpandedContent = new Container
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
- Child = new BeatmapCardDifficultyList(beatmapSet)
- },
- Expanded = { BindTarget = Expanded }
- };
-
- if (beatmapSet.HasVideo)
- leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });
-
- if (beatmapSet.HasStoryboard)
- leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
-
- if (beatmapSet.HasExplicitContent)
- {
- titleContainer.Content[0][1] = new ExplicitContentBeatmapPill
- {
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Margin = new MarginPadding { Left = 5 }
- };
- }
-
- if (beatmapSet.TrackId != null)
- {
- artistContainer.Content[0][1] = new FeaturedArtistBeatmapPill
- {
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Margin = new MarginPadding { Left = 5 }
- };
- }
-
- Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapSet.OnlineID);
+ AddInternal(DownloadTracker);
}
protected override void LoadComplete()
{
base.LoadComplete();
- downloadTracker.State.BindValueChanged(_ => updateState());
- Expanded.BindValueChanged(_ => updateState(), true);
+ DownloadTracker.State.BindValueChanged(_ => UpdateState());
+ Expanded.BindValueChanged(_ => UpdateState(), true);
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
- updateState();
+ UpdateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
- updateState();
+ UpdateState();
base.OnHoverLost(e);
}
- private LocalisableString createArtistText()
+ protected virtual void UpdateState()
{
- var romanisableArtist = new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist);
- return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
- }
+ bool showProgress = DownloadTracker.State.Value == DownloadState.Downloading || DownloadTracker.State.Value == DownloadState.Importing;
- private IEnumerable createStatistics()
- {
- if (beatmapSet.HypeStatus != null)
- yield return new HypesStatistic(beatmapSet.HypeStatus);
-
- // web does not show nominations unless hypes are also present.
- // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443
- if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null)
- yield return new NominationsStatistic(beatmapSet.NominationStatus);
-
- yield return new FavouritesStatistic(beatmapSet) { Current = favouriteState };
- yield return new PlayCountStatistic(beatmapSet);
-
- var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet);
- if (dateStatistic != null)
- yield return dateStatistic;
- }
-
- private void updateState()
- {
- bool showDetails = IsHovered || Expanded.Value;
-
- float targetWidth = width - height;
- if (showDetails)
- targetWidth = targetWidth - icon_area_width + CORNER_RADIUS;
-
- thumbnail.Dimmed.Value = showDetails;
-
- // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards.
- // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
- content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
-
- mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint);
- mainContentBackground.Dimmed.Value = showDetails;
-
- statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
-
- rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, TRANSITION_DURATION, Easing.OutQuint);
- rightAreaButtons.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
-
- foreach (var button in rightAreaButtons)
- {
- button.IdleColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Light1 : colourProvider.Background3;
- button.HoverColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Content1 : colourProvider.Foreground1;
- }
-
- bool showProgress = downloadTracker.State.Value == DownloadState.Downloading || downloadTracker.State.Value == DownloadState.Importing;
-
- idleBottomContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint);
- downloadProgressBar.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
+ IdleContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint);
+ DownloadInProgressContent.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
}
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
index 286e03e700..497283bc64 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContent.cs
@@ -139,6 +139,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private void updateState()
{
+ // Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards.
+ // This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
+ this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
+
background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
new file mode 100644
index 0000000000..7f70423ff9
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
@@ -0,0 +1,317 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Beatmaps.Drawables.Cards.Statistics;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
+using osu.Game.Overlays.BeatmapSet;
+using osuTK;
+using osu.Game.Resources.Localisation.Web;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class BeatmapCardExtra : BeatmapCard
+ {
+ protected override Drawable IdleContent => idleBottomContent;
+ protected override Drawable DownloadInProgressContent => downloadProgressBar;
+
+ private const float width = 475;
+ private const float height = 140;
+
+ [Cached]
+ private readonly BeatmapCardContent content;
+
+ private BeatmapCardThumbnail thumbnail = null!;
+ private CollapsibleButtonContainer buttonContainer = null!;
+
+ private GridContainer statisticsContainer = null!;
+
+ private FillFlowContainer idleBottomContent = null!;
+ private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ public BeatmapCardExtra(APIBeatmapSet beatmapSet, bool allowExpansion = true)
+ : base(beatmapSet, allowExpansion)
+ {
+ content = new BeatmapCardContent(height);
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(BeatmapSetOverlay? beatmapSetOverlay)
+ {
+ Width = width;
+ Height = height;
+
+ FillFlowContainer leftIconArea = null!;
+ GridContainer titleContainer = null!;
+ GridContainer artistContainer = null!;
+
+ Child = content.With(c =>
+ {
+ c.MainContent = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ thumbnail = new BeatmapCardThumbnail(BeatmapSet)
+ {
+ Name = @"Left (icon) area",
+ Size = new Vector2(height),
+ Padding = new MarginPadding { Right = CORNER_RADIUS },
+ Child = leftIconArea = new FillFlowContainer
+ {
+ Margin = new MarginPadding(5),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(1)
+ }
+ },
+ buttonContainer = new CollapsibleButtonContainer(BeatmapSet)
+ {
+ X = height - CORNER_RADIUS,
+ Width = width - height + CORNER_RADIUS,
+ FavouriteState = { BindTarget = FavouriteState },
+ ButtonsCollapsedWidth = CORNER_RADIUS,
+ ButtonsExpandedWidth = 30,
+ ButtonsPadding = new MarginPadding { Vertical = 35 },
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ titleContainer = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new[]
+ {
+ new OsuSpriteText
+ {
+ Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
+ Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
+ RelativeSizeAxes = Axes.X,
+ Truncate = true
+ },
+ Empty()
+ }
+ }
+ },
+ artistContainer = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new[]
+ {
+ new OsuSpriteText
+ {
+ Text = createArtistText(),
+ Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
+ RelativeSizeAxes = Axes.X,
+ Truncate = true
+ },
+ Empty()
+ },
+ }
+ },
+ new OsuSpriteText
+ {
+ RelativeSizeAxes = Axes.X,
+ Truncate = true,
+ Text = BeatmapSet.Source,
+ Shadow = false,
+ Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
+ Colour = colourProvider.Content2
+ },
+ }
+ },
+ new Container
+ {
+ Name = @"Bottom content",
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Children = new Drawable[]
+ {
+ idleBottomContent = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 3),
+ AlwaysPresent = true,
+ Children = new Drawable[]
+ {
+ new LinkFlowContainer(s =>
+ {
+ s.Shadow = false;
+ s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
+ }).With(d =>
+ {
+ d.AutoSizeAxes = Axes.Both;
+ d.Margin = new MarginPadding { Top = 2 };
+ d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
+ d.AddUserLink(BeatmapSet.Author);
+ }),
+ statisticsContainer = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ ColumnDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension()
+ },
+ Content = new[]
+ {
+ new Drawable[3],
+ new Drawable[3]
+ }
+ },
+ new BeatmapCardExtraInfoRow(BeatmapSet)
+ }
+ },
+ downloadProgressBar = new BeatmapCardDownloadProgressBar
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 6,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ State = { BindTarget = DownloadTracker.State },
+ Progress = { BindTarget = DownloadTracker.Progress }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ c.ExpandedContent = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
+ Child = new BeatmapCardDifficultyList(BeatmapSet)
+ };
+ c.Expanded.BindTarget = Expanded;
+ });
+
+ if (BeatmapSet.HasVideo)
+ leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });
+
+ if (BeatmapSet.HasStoryboard)
+ leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
+
+ if (BeatmapSet.HasExplicitContent)
+ {
+ titleContainer.Content[0][1] = new ExplicitContentBeatmapPill
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Margin = new MarginPadding { Left = 5 }
+ };
+ }
+
+ if (BeatmapSet.TrackId != null)
+ {
+ artistContainer.Content[0][1] = new FeaturedArtistBeatmapPill
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Margin = new MarginPadding { Left = 5 }
+ };
+ }
+
+ createStatistics();
+
+ Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
+ }
+
+ private LocalisableString createArtistText()
+ {
+ var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
+ return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
+ }
+
+ private void createStatistics()
+ {
+ BeatmapCardStatistic withMargin(BeatmapCardStatistic original)
+ {
+ original.Margin = new MarginPadding { Right = 10 };
+ return original;
+ }
+
+ statisticsContainer.Content[0][0] = withMargin(new FavouritesStatistic(BeatmapSet)
+ {
+ Current = FavouriteState,
+ });
+
+ statisticsContainer.Content[1][0] = withMargin(new PlayCountStatistic(BeatmapSet));
+
+ var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet);
+ if (hypesStatistic != null)
+ statisticsContainer.Content[0][1] = withMargin(hypesStatistic);
+
+ var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet);
+ if (nominationsStatistic != null)
+ statisticsContainer.Content[1][1] = withMargin(nominationsStatistic);
+
+ var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
+ if (dateStatistic != null)
+ statisticsContainer.Content[0][2] = withMargin(dateStatistic);
+ }
+
+ protected override void UpdateState()
+ {
+ base.UpdateState();
+
+ bool showDetails = IsHovered || Expanded.Value;
+
+ buttonContainer.ShowDetails.Value = showDetails;
+ thumbnail.Dimmed.Value = showDetails;
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs
index 0a9d98e621..2d411ad344 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtraInfoRow.cs
@@ -1,21 +1,28 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards
{
- public class BeatmapCardExtraInfoRow : HoverHandlingContainer
+ public class BeatmapCardExtraInfoRow : CompositeDrawable
{
+ [Resolved(CanBeNull = true)]
+ private BeatmapCardContent? content { get; set; }
+
public BeatmapCardExtraInfoRow(APIBeatmapSet beatmapSet)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Child = new FillFlowContainer
+ InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -39,5 +46,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}
};
}
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ content?.ExpandAfterDelay();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ if (content?.Expanded.Value == false)
+ content.CancelExpand();
+
+ base.OnHoverLost(e);
+ }
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
new file mode 100644
index 0000000000..edca310db9
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
@@ -0,0 +1,286 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Beatmaps.Drawables.Cards.Statistics;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
+using osu.Game.Overlays.BeatmapSet;
+using osuTK;
+using osu.Game.Resources.Localisation.Web;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class BeatmapCardNormal : BeatmapCard
+ {
+ protected override Drawable IdleContent => idleBottomContent;
+ protected override Drawable DownloadInProgressContent => downloadProgressBar;
+
+ private const float width = 408;
+ private const float height = 100;
+
+ [Cached]
+ private readonly BeatmapCardContent content;
+
+ private BeatmapCardThumbnail thumbnail = null!;
+ private CollapsibleButtonContainer buttonContainer = null!;
+
+ private FillFlowContainer statisticsContainer = null!;
+
+ private FillFlowContainer idleBottomContent = null!;
+ private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ public BeatmapCardNormal(APIBeatmapSet beatmapSet, bool allowExpansion = true)
+ : base(beatmapSet, allowExpansion)
+ {
+ content = new BeatmapCardContent(height);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Width = width;
+ Height = height;
+
+ FillFlowContainer leftIconArea = null!;
+ GridContainer titleContainer = null!;
+ GridContainer artistContainer = null!;
+
+ Child = content.With(c =>
+ {
+ c.MainContent = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ thumbnail = new BeatmapCardThumbnail(BeatmapSet)
+ {
+ Name = @"Left (icon) area",
+ Size = new Vector2(height),
+ Padding = new MarginPadding { Right = CORNER_RADIUS },
+ Child = leftIconArea = new FillFlowContainer
+ {
+ Margin = new MarginPadding(5),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(1)
+ }
+ },
+ buttonContainer = new CollapsibleButtonContainer(BeatmapSet)
+ {
+ X = height - CORNER_RADIUS,
+ Width = width - height + CORNER_RADIUS,
+ FavouriteState = { BindTarget = FavouriteState },
+ ButtonsCollapsedWidth = CORNER_RADIUS,
+ ButtonsExpandedWidth = 30,
+ ButtonsPadding = new MarginPadding { Vertical = 17.5f },
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ titleContainer = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new[]
+ {
+ new OsuSpriteText
+ {
+ Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
+ Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
+ RelativeSizeAxes = Axes.X,
+ Truncate = true
+ },
+ Empty()
+ }
+ }
+ },
+ artistContainer = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize)
+ },
+ Content = new[]
+ {
+ new[]
+ {
+ new OsuSpriteText
+ {
+ Text = createArtistText(),
+ Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
+ RelativeSizeAxes = Axes.X,
+ Truncate = true
+ },
+ Empty()
+ },
+ }
+ },
+ new LinkFlowContainer(s =>
+ {
+ s.Shadow = false;
+ s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
+ }).With(d =>
+ {
+ d.AutoSizeAxes = Axes.Both;
+ d.Margin = new MarginPadding { Top = 2 };
+ d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
+ d.AddUserLink(BeatmapSet.Author);
+ }),
+ }
+ },
+ new Container
+ {
+ Name = @"Bottom content",
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Children = new Drawable[]
+ {
+ idleBottomContent = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 3),
+ AlwaysPresent = true,
+ Children = new Drawable[]
+ {
+ statisticsContainer = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ Alpha = 0,
+ AlwaysPresent = true,
+ ChildrenEnumerable = createStatistics()
+ },
+ new BeatmapCardExtraInfoRow(BeatmapSet)
+ }
+ },
+ downloadProgressBar = new BeatmapCardDownloadProgressBar
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 6,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ State = { BindTarget = DownloadTracker.State },
+ Progress = { BindTarget = DownloadTracker.Progress }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+ c.ExpandedContent = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
+ Child = new BeatmapCardDifficultyList(BeatmapSet)
+ };
+ c.Expanded.BindTarget = Expanded;
+ });
+
+ if (BeatmapSet.HasVideo)
+ leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });
+
+ if (BeatmapSet.HasStoryboard)
+ leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
+
+ if (BeatmapSet.HasExplicitContent)
+ {
+ titleContainer.Content[0][1] = new ExplicitContentBeatmapPill
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Margin = new MarginPadding { Left = 5 }
+ };
+ }
+
+ if (BeatmapSet.TrackId != null)
+ {
+ artistContainer.Content[0][1] = new FeaturedArtistBeatmapPill
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Margin = new MarginPadding { Left = 5 }
+ };
+ }
+ }
+
+ private LocalisableString createArtistText()
+ {
+ var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
+ return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
+ }
+
+ private IEnumerable createStatistics()
+ {
+ var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet);
+ if (hypesStatistic != null)
+ yield return hypesStatistic;
+
+ var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet);
+ if (nominationsStatistic != null)
+ yield return nominationsStatistic;
+
+ yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState };
+ yield return new PlayCountStatistic(BeatmapSet);
+
+ var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
+ if (dateStatistic != null)
+ yield return dateStatistic;
+ }
+
+ protected override void UpdateState()
+ {
+ base.UpdateState();
+
+ bool showDetails = IsHovered || Expanded.Value;
+
+ buttonContainer.ShowDetails.Value = showDetails;
+ thumbnail.Dimmed.Value = showDetails;
+
+ statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs
new file mode 100644
index 0000000000..3a2cb80a8d
--- /dev/null
+++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs
@@ -0,0 +1,184 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps.Drawables.Cards.Buttons;
+using osu.Game.Graphics;
+using osu.Game.Online;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays;
+
+namespace osu.Game.Beatmaps.Drawables.Cards
+{
+ public class CollapsibleButtonContainer : Container
+ {
+ public Bindable ShowDetails = new Bindable();
+ public Bindable FavouriteState = new Bindable();
+
+ private readonly BeatmapDownloadTracker downloadTracker;
+
+ private float buttonsExpandedWidth;
+
+ public float ButtonsExpandedWidth
+ {
+ get => buttonsExpandedWidth;
+ set
+ {
+ buttonsExpandedWidth = value;
+ buttonArea.Width = value;
+ if (IsLoaded)
+ updateState();
+ }
+ }
+
+ private float buttonsCollapsedWidth;
+
+ public float ButtonsCollapsedWidth
+ {
+ get => buttonsCollapsedWidth;
+ set
+ {
+ buttonsCollapsedWidth = value;
+ if (IsLoaded)
+ updateState();
+ }
+ }
+
+ public MarginPadding ButtonsPadding
+ {
+ get => buttons.Padding;
+ set => buttons.Padding = value;
+ }
+
+ protected override Container Content => mainContent;
+
+ private readonly Container background;
+
+ private readonly Container buttonArea;
+ private readonly Container buttons;
+
+ private readonly Container mainArea;
+ private readonly Container mainContent;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ public CollapsibleButtonContainer(APIBeatmapSet beatmapSet)
+ {
+ downloadTracker = new BeatmapDownloadTracker(beatmapSet);
+
+ RelativeSizeAxes = Axes.Y;
+ Masking = true;
+ CornerRadius = BeatmapCard.CORNER_RADIUS;
+
+ InternalChildren = new Drawable[]
+ {
+ downloadTracker,
+ background = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ // workaround for masking artifacts at the top & bottom of card,
+ // which become especially visible on downloaded beatmaps (when the icon area has a lime background).
+ Padding = new MarginPadding { Vertical = 1 },
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.White
+ },
+ },
+ buttonArea = new Container
+ {
+ Name = @"Right (button) area",
+ RelativeSizeAxes = Axes.Y,
+ Origin = Anchor.TopRight,
+ Anchor = Anchor.TopRight,
+ Child = buttons = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new BeatmapCardIconButton[]
+ {
+ new FavouriteButton(beatmapSet)
+ {
+ Current = FavouriteState,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ },
+ new DownloadButton(beatmapSet)
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ State = { BindTarget = downloadTracker.State }
+ },
+ new GoToBeatmapButton(beatmapSet)
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ State = { BindTarget = downloadTracker.State }
+ }
+ }
+ }
+ },
+ mainArea = new Container
+ {
+ Name = @"Main content",
+ RelativeSizeAxes = Axes.Y,
+ CornerRadius = BeatmapCard.CORNER_RADIUS,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new BeatmapCardContentBackground(beatmapSet)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Dimmed = { BindTarget = ShowDetails }
+ },
+ mainContent = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding
+ {
+ Horizontal = 10,
+ Vertical = 4
+ },
+ }
+ }
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ downloadTracker.State.BindValueChanged(_ => updateState());
+ ShowDetails.BindValueChanged(_ => updateState(), true);
+ FinishTransforms(true);
+ }
+
+ private void updateState()
+ {
+ float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth);
+
+ mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+
+ background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ buttons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+
+ foreach (var button in buttons)
+ {
+ button.IdleColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Light1 : colourProvider.Background3;
+ button.HoverColour = downloadTracker.State.Value != DownloadState.LocallyAvailable ? colourProvider.Content1 : colourProvider.Foreground1;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs
index 3fe31c7a41..521d1a5f21 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/HypesStatistic.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
@@ -12,11 +14,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
///
public class HypesStatistic : BeatmapCardStatistic
{
- public HypesStatistic(BeatmapSetHypeStatus hypeStatus)
+ private HypesStatistic(BeatmapSetHypeStatus hypeStatus)
{
Icon = FontAwesome.Solid.Bullhorn;
Text = hypeStatus.Current.ToLocalisableString();
TooltipText = BeatmapsStrings.HypeRequiredText(hypeStatus.Current.ToLocalisableString(), hypeStatus.Required.ToLocalisableString());
}
+
+ public static HypesStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetOnlineInfo)
+ => beatmapSetOnlineInfo.HypeStatus == null ? null : new HypesStatistic(beatmapSetOnlineInfo.HypeStatus);
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs
index f09269a615..23bd6ef0a9 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
@@ -12,11 +14,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
///
public class NominationsStatistic : BeatmapCardStatistic
{
- public NominationsStatistic(BeatmapSetNominationStatus nominationStatus)
+ private NominationsStatistic(BeatmapSetNominationStatus nominationStatus)
{
Icon = FontAwesome.Solid.ThumbsUp;
Text = nominationStatus.Current.ToLocalisableString();
TooltipText = BeatmapsStrings.NominationsRequiredText(nominationStatus.Current.ToLocalisableString(), nominationStatus.Required.ToLocalisableString());
}
+
+ public static NominationsStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetOnlineInfo)
+ // web does not show nominations unless hypes are also present.
+ // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443
+ => beatmapSetOnlineInfo.HypeStatus == null || beatmapSetOnlineInfo.NominationStatus == null ? null : new NominationsStatistic(beatmapSetOnlineInfo.NominationStatus);
}
}
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index ac94c39bd2..837ee7e634 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.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 osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
@@ -13,18 +12,27 @@ namespace osu.Game.Configuration
///
public class SessionStatics : InMemoryConfigManager
{
- protected override void InitialiseDefaults() => ResetValues();
-
- public void ResetValues()
+ protected override void InitialiseDefaults()
{
- ensureDefault(SetDefault(Static.LoginOverlayDisplayed, false));
- ensureDefault(SetDefault(Static.MutedAudioNotificationShownOnce, false));
- ensureDefault(SetDefault(Static.LowBatteryNotificationShownOnce, false));
- ensureDefault(SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null));
- ensureDefault(SetDefault(Static.SeasonalBackgrounds, null));
+ SetDefault(Static.LoginOverlayDisplayed, false);
+ SetDefault(Static.MutedAudioNotificationShownOnce, false);
+ SetDefault(Static.LowBatteryNotificationShownOnce, false);
+ SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
+ SetDefault(Static.SeasonalBackgrounds, null);
}
- private void ensureDefault(Bindable bindable) => bindable.SetDefault();
+ ///
+ /// Revert statics to their defaults after being idle for appropriate amount of time.
+ ///
+ ///
+ /// This only affects a subset of statics which the user would expect to have reset after a break.
+ ///
+ public void ResetAfterInactivity()
+ {
+ GetBindable(Static.LoginOverlayDisplayed).SetDefault();
+ GetBindable(Static.MutedAudioNotificationShownOnce).SetDefault();
+ GetBindable(Static.LowBatteryNotificationShownOnce).SetDefault();
+ }
}
public enum Static
diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs
index c6f8244494..06edc3e2da 100644
--- a/osu.Game/Database/BeatmapLookupCache.cs
+++ b/osu.Game/Database/BeatmapLookupCache.cs
@@ -6,20 +6,13 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
-using osu.Framework.Allocation;
-using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Database
{
- // This class is based on `UserLookupCache` which is well tested.
- // If modifications are to be made here, a base abstract implementation should likely be created and shared between the two.
- public class BeatmapLookupCache : MemoryCachingComponent
+ public class BeatmapLookupCache : OnlineLookupCache
{
- [Resolved]
- private IAPIProvider api { get; set; }
-
///
/// Perform an API lookup on the specified beatmap, populating a model.
///
@@ -27,7 +20,7 @@ namespace osu.Game.Database
/// An optional cancellation token.
/// The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.
[ItemCanBeNull]
- public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => GetAsync(beatmapId, token);
+ public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
///
/// Perform an API lookup on the specified beatmaps, populating a model.
@@ -35,115 +28,10 @@ namespace osu.Game.Database
/// The beatmaps to lookup.
/// An optional cancellation token.
/// The populated beatmaps. May include null results for failed retrievals.
- public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default)
- {
- var beatmapLookupTasks = new List>();
+ public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
- foreach (int u in beatmapIds)
- {
- beatmapLookupTasks.Add(GetBeatmapAsync(u, token).ContinueWith(task =>
- {
- if (!task.IsCompletedSuccessfully)
- return null;
+ protected override GetBeatmapsRequest CreateRequest(IEnumerable ids) => new GetBeatmapsRequest(ids.ToArray());
- return task.Result;
- }, token));
- }
-
- return Task.WhenAll(beatmapLookupTasks);
- }
-
- protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default)
- => await queryBeatmap(lookup).ConfigureAwait(false);
-
- private readonly Queue<(int id, TaskCompletionSource)> pendingBeatmapTasks = new Queue<(int, TaskCompletionSource)>();
- private Task pendingRequestTask;
- private readonly object taskAssignmentLock = new object();
-
- private Task queryBeatmap(int beatmapId)
- {
- lock (taskAssignmentLock)
- {
- var tcs = new TaskCompletionSource();
-
- // Add to the queue.
- pendingBeatmapTasks.Enqueue((beatmapId, tcs));
-
- // Create a request task if there's not already one.
- if (pendingRequestTask == null)
- createNewTask();
-
- return tcs.Task;
- }
- }
-
- private void performLookup()
- {
- // contains at most 50 unique beatmap IDs from beatmapTasks, which is used to perform the lookup.
- var beatmapTasks = new Dictionary>>();
-
- // Grab at most 50 unique beatmap IDs from the queue.
- lock (taskAssignmentLock)
- {
- while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 50)
- {
- (int id, TaskCompletionSource task) next = pendingBeatmapTasks.Dequeue();
-
- // Perform a secondary check for existence, in case the beatmap was queried in a previous batch.
- if (CheckExists(next.id, out var existing))
- next.task.SetResult(existing);
- else
- {
- if (beatmapTasks.TryGetValue(next.id, out var tasks))
- tasks.Add(next.task);
- else
- beatmapTasks[next.id] = new List> { next.task };
- }
- }
- }
-
- if (beatmapTasks.Count == 0)
- return;
-
- // Query the beatmaps.
- var request = new GetBeatmapsRequest(beatmapTasks.Keys.ToArray());
-
- // rather than queueing, we maintain our own single-threaded request stream.
- // todo: we probably want retry logic here.
- api.Perform(request);
-
- // Create a new request task if there's still more beatmaps to query.
- lock (taskAssignmentLock)
- {
- pendingRequestTask = null;
- if (pendingBeatmapTasks.Count > 0)
- createNewTask();
- }
-
- List foundBeatmaps = request.Response?.Beatmaps;
-
- if (foundBeatmaps != null)
- {
- foreach (var beatmap in foundBeatmaps)
- {
- if (beatmapTasks.TryGetValue(beatmap.OnlineID, out var tasks))
- {
- foreach (var task in tasks)
- task.SetResult(beatmap);
-
- beatmapTasks.Remove(beatmap.OnlineID);
- }
- }
- }
-
- // if any tasks remain which were not satisfied, return null.
- foreach (var tasks in beatmapTasks.Values)
- {
- foreach (var task in tasks)
- task.SetResult(null);
- }
- }
-
- private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
+ protected override IEnumerable RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
}
}
diff --git a/osu.Game/Database/OnlineLookupCache.cs b/osu.Game/Database/OnlineLookupCache.cs
new file mode 100644
index 0000000000..5eb9fa24fa
--- /dev/null
+++ b/osu.Game/Database/OnlineLookupCache.cs
@@ -0,0 +1,170 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Game.Online.API;
+
+namespace osu.Game.Database
+{
+ public abstract class OnlineLookupCache : MemoryCachingComponent
+ where TLookup : IEquatable
+ where TValue : class, IHasOnlineID
+ where TRequest : APIRequest
+ {
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ ///
+ /// Creates an to retrieve the values for a given collection of s.
+ ///
+ /// The IDs to perform the lookup with.
+ protected abstract TRequest CreateRequest(IEnumerable ids);
+
+ ///
+ /// Retrieves a list of s from a successful created by .
+ ///
+ [CanBeNull]
+ protected abstract IEnumerable RetrieveResults(TRequest request);
+
+ ///
+ /// Perform a lookup using the specified , populating a .
+ ///
+ /// The ID to lookup.
+ /// An optional cancellation token.
+ /// The populated , or null if the value does not exist or the request could not be satisfied.
+ [ItemCanBeNull]
+ protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
+
+ ///
+ /// Perform an API lookup on the specified , populating a .
+ ///
+ /// The IDs to lookup.
+ /// An optional cancellation token.
+ /// The populated values. May include null results for failed retrievals.
+ protected Task LookupAsync(TLookup[] ids, CancellationToken token = default)
+ {
+ var lookupTasks = new List>();
+
+ foreach (var id in ids)
+ {
+ lookupTasks.Add(LookupAsync(id, token).ContinueWith(task =>
+ {
+ if (!task.IsCompletedSuccessfully)
+ return null;
+
+ return task.Result;
+ }, token));
+ }
+
+ return Task.WhenAll(lookupTasks);
+ }
+
+ // cannot be sealed due to test usages (see TestUserLookupCache).
+ protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default)
+ => await queryValue(lookup).ConfigureAwait(false);
+
+ private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>();
+ private Task pendingRequestTask;
+ private readonly object taskAssignmentLock = new object();
+
+ private Task queryValue(TLookup id)
+ {
+ lock (taskAssignmentLock)
+ {
+ var tcs = new TaskCompletionSource();
+
+ // Add to the queue.
+ pendingTasks.Enqueue((id, tcs));
+
+ // Create a request task if there's not already one.
+ if (pendingRequestTask == null)
+ createNewTask();
+
+ return tcs.Task;
+ }
+ }
+
+ private void performLookup()
+ {
+ // contains at most 50 unique IDs from tasks, which is used to perform the lookup.
+ var nextTaskBatch = new Dictionary>>();
+
+ // Grab at most 50 unique IDs from the queue.
+ lock (taskAssignmentLock)
+ {
+ while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50)
+ {
+ (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue();
+
+ // Perform a secondary check for existence, in case the value was queried in a previous batch.
+ if (CheckExists(next.id, out var existing))
+ next.task.SetResult(existing);
+ else
+ {
+ if (nextTaskBatch.TryGetValue(next.id, out var tasks))
+ tasks.Add(next.task);
+ else
+ nextTaskBatch[next.id] = new List> { next.task };
+ }
+ }
+ }
+
+ if (nextTaskBatch.Count == 0)
+ {
+ finishPendingTask();
+ return;
+ }
+
+ // Query the values.
+ var request = CreateRequest(nextTaskBatch.Keys.ToArray());
+
+ // rather than queueing, we maintain our own single-threaded request stream.
+ // todo: we probably want retry logic here.
+ api.Perform(request);
+
+ finishPendingTask();
+
+ var foundValues = RetrieveResults(request);
+
+ if (foundValues != null)
+ {
+ foreach (var value in foundValues)
+ {
+ if (nextTaskBatch.TryGetValue(value.OnlineID, out var tasks))
+ {
+ foreach (var task in tasks)
+ task.SetResult(value);
+
+ nextTaskBatch.Remove(value.OnlineID);
+ }
+ }
+ }
+
+ // if any tasks remain which were not satisfied, return null.
+ foreach (var tasks in nextTaskBatch.Values)
+ {
+ foreach (var task in tasks)
+ task.SetResult(null);
+ }
+ }
+
+ private void finishPendingTask()
+ {
+ // Create a new request task if there's still more values to query.
+ lock (taskAssignmentLock)
+ {
+ pendingRequestTask = null;
+ if (pendingTasks.Count > 0)
+ createNewTask();
+ }
+ }
+
+ private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
+ }
+}
diff --git a/osu.Game/Database/RealmLiveUnmanaged.cs b/osu.Game/Database/RealmLiveUnmanaged.cs
index 5a69898206..ea50ccc1ff 100644
--- a/osu.Game/Database/RealmLiveUnmanaged.cs
+++ b/osu.Game/Database/RealmLiveUnmanaged.cs
@@ -26,6 +26,8 @@ namespace osu.Game.Database
public bool Equals(ILive? other) => ID == other?.ID;
+ public override string ToString() => Value.ToString();
+
public Guid ID => Value.ID;
public void PerformRead(Action perform) => perform(Value);
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index 26f4e9fb3b..5fdd80892d 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -6,18 +6,13 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
-using osu.Framework.Allocation;
-using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Database
{
- public class UserLookupCache : MemoryCachingComponent
+ public class UserLookupCache : OnlineLookupCache
{
- [Resolved]
- private IAPIProvider api { get; set; }
-
///
/// Perform an API lookup on the specified user, populating a model.
///
@@ -25,7 +20,7 @@ namespace osu.Game.Database
/// An optional cancellation token.
/// The populated user, or null if the user does not exist or the request could not be satisfied.
[ItemCanBeNull]
- public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
+ public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
///
/// Perform an API lookup on the specified users, populating a model.
@@ -33,115 +28,10 @@ namespace osu.Game.Database
/// The users to lookup.
/// An optional cancellation token.
/// The populated users. May include null results for failed retrievals.
- public Task GetUsersAsync(int[] userIds, CancellationToken token = default)
- {
- var userLookupTasks = new List>();
+ public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
- foreach (int u in userIds)
- {
- userLookupTasks.Add(GetUserAsync(u, token).ContinueWith(task =>
- {
- if (!task.IsCompletedSuccessfully)
- return null;
+ protected override GetUsersRequest CreateRequest(IEnumerable ids) => new GetUsersRequest(ids.ToArray());
- return task.Result;
- }, token));
- }
-
- return Task.WhenAll(userLookupTasks);
- }
-
- protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default)
- => await queryUser(lookup).ConfigureAwait(false);
-
- private readonly Queue<(int id, TaskCompletionSource)> pendingUserTasks = new Queue<(int, TaskCompletionSource)>();
- private Task pendingRequestTask;
- private readonly object taskAssignmentLock = new object();
-
- private Task queryUser(int userId)
- {
- lock (taskAssignmentLock)
- {
- var tcs = new TaskCompletionSource();
-
- // Add to the queue.
- pendingUserTasks.Enqueue((userId, tcs));
-
- // Create a request task if there's not already one.
- if (pendingRequestTask == null)
- createNewTask();
-
- return tcs.Task;
- }
- }
-
- private void performLookup()
- {
- // contains at most 50 unique user IDs from userTasks, which is used to perform the lookup.
- var userTasks = new Dictionary>>();
-
- // Grab at most 50 unique user IDs from the queue.
- lock (taskAssignmentLock)
- {
- while (pendingUserTasks.Count > 0 && userTasks.Count < 50)
- {
- (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue();
-
- // Perform a secondary check for existence, in case the user was queried in a previous batch.
- if (CheckExists(next.id, out var existing))
- next.task.SetResult(existing);
- else
- {
- if (userTasks.TryGetValue(next.id, out var tasks))
- tasks.Add(next.task);
- else
- userTasks[next.id] = new List> { next.task };
- }
- }
- }
-
- if (userTasks.Count == 0)
- return;
-
- // Query the users.
- var request = new GetUsersRequest(userTasks.Keys.ToArray());
-
- // rather than queueing, we maintain our own single-threaded request stream.
- // todo: we probably want retry logic here.
- api.Perform(request);
-
- // Create a new request task if there's still more users to query.
- lock (taskAssignmentLock)
- {
- pendingRequestTask = null;
- if (pendingUserTasks.Count > 0)
- createNewTask();
- }
-
- List foundUsers = request.Response?.Users;
-
- if (foundUsers != null)
- {
- foreach (var user in foundUsers)
- {
- if (userTasks.TryGetValue(user.Id, out var tasks))
- {
- foreach (var task in tasks)
- task.SetResult(user);
-
- userTasks.Remove(user.Id);
- }
- }
- }
-
- // if any tasks remain which were not satisfied, return null.
- foreach (var tasks in userTasks.Values)
- {
- foreach (var task in tasks)
- task.SetResult(null);
- }
- }
-
- private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
+ protected override IEnumerable RetrieveResults(GetUsersRequest request) => request.Response?.Users;
}
}
diff --git a/osu.Game/Online/Rooms/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs
index 992011da3c..e78f91f20b 100644
--- a/osu.Game/Online/Rooms/PlaylistExtensions.cs
+++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs
@@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
+using System.Collections.Generic;
using System.Linq;
using Humanizer;
using Humanizer.Localisation;
@@ -10,6 +13,33 @@ namespace osu.Game.Online.Rooms
{
public static class PlaylistExtensions
{
+ ///
+ /// Returns all historical/expired items from the , in the order in which they were played.
+ ///
+ public static IEnumerable GetHistoricalItems(this IEnumerable playlist)
+ => playlist.Where(item => item.Expired).OrderBy(item => item.PlayedAt);
+
+ ///
+ /// Returns all non-expired items from the , in the order in which they are to be played.
+ ///
+ public static IEnumerable GetUpcomingItems(this IEnumerable playlist)
+ => playlist.Where(item => !item.Expired).OrderBy(item => item.PlaylistOrder);
+
+ ///
+ /// Returns the first non-expired in playlist order from the supplied ,
+ /// or the last-played if all items are expired,
+ /// or if was empty.
+ ///
+ public static PlaylistItem? GetCurrentItem(this ICollection playlist)
+ {
+ if (playlist.Count == 0)
+ return null;
+
+ return playlist.All(item => item.Expired)
+ ? GetHistoricalItems(playlist).Last()
+ : GetUpcomingItems(playlist).First();
+ }
+
public static string GetTotalDuration(this BindableList playlist) =>
playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2);
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index a4471b56b9..c5a465ae96 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -680,7 +680,7 @@ namespace osu.Game
sessionIdleTracker.IsIdle.BindValueChanged(idle =>
{
if (idle.NewValue)
- SessionStatics.ResetValues();
+ SessionStatics.ResetAfterInactivity();
});
Add(sessionIdleTracker);
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 6b27dbf847..c4d1d62250 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Overlays
private Drawable currentContent;
private Container panelTarget;
- private FillFlowContainer foundContent;
+ private FillFlowContainer foundContent;
private NotFoundDrawable notFoundContent;
private SupporterRequiredDrawable supporterRequiredContent;
private BeatmapListingFilterControl filterControl;
@@ -78,7 +78,7 @@ namespace osu.Game.Overlays
Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[]
{
- foundContent = new FillFlowContainer(),
+ foundContent = new FillFlowContainer(),
notFoundContent = new NotFoundDrawable(),
supporterRequiredContent = new SupporterRequiredDrawable(),
}
@@ -135,7 +135,7 @@ namespace osu.Game.Overlays
return;
}
- var newPanels = searchResult.Results.Select(b => new BeatmapCard(b)
+ var newPanels = searchResult.Results.Select(b => new BeatmapCardNormal(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@@ -152,7 +152,7 @@ namespace osu.Game.Overlays
// spawn new children with the contained so we only clear old content at the last moment.
// reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
- var content = new ReverseChildIDFillFlowContainer
+ var content = new ReverseChildIDFillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
index d6e515d8a1..d195babcbf 100644
--- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
@@ -42,7 +42,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateStatistics(UserStatistics statistics)
{
- int[] userRanks = statistics?.RankHistory?.Data;
+ // checking both IsRanked and RankHistory is required.
+ // see https://github.com/ppy/osu-web/blob/154ceafba0f35a1dd935df53ec98ae2ea5615f9f/resources/assets/lib/profile-page/rank-chart.tsx#L46
+ int[] userRanks = statistics?.IsRanked == true ? statistics.RankHistory?.Data : null;
Data = userRanks?.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray();
}
diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
index e46e503dfa..d39074bd49 100644
--- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
protected override Drawable CreateDrawableItem(APIBeatmapSet model) => model.OnlineID > 0
- ? new BeatmapCard(model)
+ ? new BeatmapCardNormal(model)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
index bcfc2499b9..fb01656c24 100644
--- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
@@ -136,12 +136,12 @@ namespace osu.Game.Overlays.Rankings
{
new ScoresTable(1, response.Users),
// reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
- new ReverseChildIDFillFlowContainer
+ new ReverseChildIDFillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(10),
- Children = response.BeatmapSets.Select(b => new BeatmapCard(b)
+ Children = response.BeatmapSets.Select(b => new BeatmapCardNormal(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index 1839b0507d..6426d33e99 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -177,8 +177,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
else
circle.Colour = colour;
- var col = circle.Colour.TopLeft.Linear;
- colouredComponents.Colour = OsuColour.ForegroundTextColourFor(col);
+ var averageColour = Interpolation.ValueAt(0.5, circle.Colour.TopLeft, circle.Colour.TopRight, 0, 1);
+ colouredComponents.Colour = OsuColour.ForegroundTextColourFor(averageColour);
}
private SamplePointPiece sampleOverrideDisplay;
diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs
index e2ba0b03b0..d144e1e3a9 100644
--- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs
@@ -1,10 +1,10 @@
// 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.Game.Beatmaps.Drawables;
+using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components
{
@@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
private void updateBeatmap()
{
- sprite.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value;
+ sprite.Beatmap.Value = Playlist.GetCurrentItem()?.Beatmap.Value;
}
protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs
index 6c00ca2e81..926c35c5da 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs
@@ -3,7 +3,6 @@
#nullable enable
-using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Screens;
using osu.Game.Online.Rooms;
@@ -20,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
public LoungeBackgroundScreen()
{
SelectedRoom.BindValueChanged(onSelectedRoomChanged);
- playlist.BindCollectionChanged((_, __) => PlaylistItem = playlist.FirstOrDefault());
+ playlist.BindCollectionChanged((_, __) => PlaylistItem = playlist.GetCurrentItem());
}
private void onSelectedRoomChanged(ValueChangedEvent room)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 6895608c8e..af83543b16 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
@@ -434,6 +435,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void handleRoomLost() => Schedule(() =>
{
+ Logger.Log($"{this} exiting due to loss of room or connection");
+
if (this.IsCurrentScreen())
this.Exit();
else
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
index c3190cd845..48f153ecbe 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
@@ -87,7 +87,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods)
{
RelativeSizeAxes = Axes.Both,
- Child = stack = new OsuScreenStack()
+ Child = stack = new OsuScreenStack
+ {
+ Name = nameof(PlayerArea),
+ }
};
stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock)));
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs
index 6b111d76a5..c833621fbc 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
@@ -73,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay
private IBindable subScreenSelectedItem { get; set; }
///
- /// The currently selected item in the , or the last item from
+ /// The currently selected item in the , or the current item from
/// if this is not within a .
///
protected readonly Bindable SelectedItem = new Bindable();
@@ -88,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay
protected virtual void UpdateSelectedItem()
=> SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null
- ? Playlist.LastOrDefault()
+ ? Playlist.GetCurrentItem()
: subScreenSelectedItem.Value;
}
}
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 19153521cd..bf1699dca0 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -95,6 +95,8 @@ namespace osu.Game.Screens.OnlinePlay
private void forcefullyExit()
{
+ Logger.Log($"{this} forcefully exiting due to loss of API connection");
+
// This is temporary since we don't currently have a way to force screens to be exited
if (this.IsCurrentScreen())
{
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
index c7b06a3a2c..1f08cb8aa7 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
@@ -40,9 +40,16 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
if (gameplayClockContainer != null)
gameplayClockContainer.OnSeek += Clear;
- processor.NewJudgement += OnNewJudgement;
+ processor.NewJudgement += processorNewJudgement;
}
+ // Scheduled as meter implementations are likely going to change/add drawables when reacting to this.
+ private void processorNewJudgement(JudgementResult j) => Schedule(() => OnNewJudgement(j));
+
+ ///
+ /// Fired when a new judgement arrives.
+ ///
+ /// The new judgement.
protected abstract void OnNewJudgement(JudgementResult judgement);
protected Color4 GetColourForHitResult(HitResult result)
@@ -84,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
base.Dispose(isDisposing);
if (processor != null)
- processor.NewJudgement -= OnNewJudgement;
+ processor.NewJudgement -= processorNewJudgement;
if (gameplayClockContainer != null)
gameplayClockContainer.OnSeek -= Clear;
diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs
index ba5663bfa3..b530965269 100644
--- a/osu.Game/Screens/Play/SoloSpectator.cs
+++ b/osu.Game/Screens/Play/SoloSpectator.cs
@@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play
onlineBeatmapRequest.Success += beatmapSet => Schedule(() =>
{
this.beatmapSet = beatmapSet;
- beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet, allowExpansion: false);
+ beatmapPanelContainer.Child = new BeatmapCardNormal(this.beatmapSet, allowExpansion: false);
checkForAutomaticDownload();
});
diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index 5656704abf..7607122ef0 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies;
- public bool RoomJoined => RoomManager.RoomJoined;
+ public bool RoomJoined => Client.RoomJoined;
private readonly bool joinRoom;
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 4e0cfe405e..5b08b6b835 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -31,9 +31,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
private readonly Bindable isConnected = new Bindable(true);
public new Room? APIRoom => base.APIRoom;
-
public Action? RoomSetupAction;
+ public bool RoomJoined { get; private set; }
+
[Resolved]
private IAPIProvider api { get; set; } = null!;
@@ -49,7 +50,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex];
private int currentIndex;
-
private long lastPlaylistItemId;
public TestMultiplayerClient(TestMultiplayerRoomManager roomManager)
@@ -217,9 +217,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
// emulate the server sending this after the join room. scheduler required to make sure the join room event is fired first (in Join).
changeMatchType(Room.Settings.MatchType).Wait();
+
+ RoomJoined = true;
}
- protected override Task LeaveRoomInternal() => Task.CompletedTask;
+ protected override Task LeaveRoomInternal()
+ {
+ RoomJoined = false;
+ return Task.CompletedTask;
+ }
public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
index a1f010f082..296db3152d 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
@@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
///
public class TestMultiplayerRoomManager : MultiplayerRoomManager
{
- public bool RoomJoined { get; private set; }
-
private readonly TestRoomRequestsHandler requestsHandler;
public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler)
@@ -29,28 +27,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
public IReadOnlyList ServerSideRooms => requestsHandler.ServerSideRooms;
public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null)
- {
- base.CreateRoom(room, r =>
- {
- onSuccess?.Invoke(r);
- RoomJoined = true;
- }, onError);
- }
+ => base.CreateRoom(room, r => onSuccess?.Invoke(r), onError);
public override void JoinRoom(Room room, string password = null, Action onSuccess = null, Action onError = null)
- {
- base.JoinRoom(room, password, r =>
- {
- onSuccess?.Invoke(r);
- RoomJoined = true;
- }, onError);
- }
-
- public override void PartRoom()
- {
- base.PartRoom();
- RoomJoined = false;
- }
+ => base.JoinRoom(room, password, r => onSuccess?.Invoke(r), onError);
///
/// Adds a room to a local "server-side" list that's returned when a is fired.
diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
index 752794d25a..b7aa8af4aa 100644
--- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Testing;
using osu.Framework.Testing.Input;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
@@ -11,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Tests.Visual
{
@@ -115,6 +119,25 @@ namespace osu.Game.Tests.Visual
});
}
+ ///
+ /// Wait for a button to become enabled, then click it.
+ ///
+ ///
+ protected void ClickButtonWhenEnabled()
+ where T : Drawable
+ {
+ if (typeof(T) == typeof(Button))
+ AddUntilStep($"wait for {typeof(T).Name} enabled", () => (this.ChildrenOfType().Single() as Button)?.Enabled.Value == true);
+ else
+ AddUntilStep($"wait for {typeof(T).Name} enabled", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Enabled.Value);
+
+ AddStep($"click {typeof(T).Name}", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.Click(MouseButton.Left);
+ });
+ }
+
protected override void Update()
{
base.Update();
diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs
index aa46b516bf..c44a848275 100644
--- a/osu.Game/Tests/Visual/ScreenTestScene.cs
+++ b/osu.Game/Tests/Visual/ScreenTestScene.cs
@@ -29,7 +29,11 @@ namespace osu.Game.Tests.Visual
{
base.Content.AddRange(new Drawable[]
{
- Stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
+ Stack = new OsuScreenStack
+ {
+ Name = nameof(ScreenTestScene),
+ RelativeSizeAxes = Axes.Both
+ },
content = new Container { RelativeSizeAxes = Axes.Both },
DialogOverlay = new DialogOverlay()
});
diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs
index c690447256..f8d26fe421 100644
--- a/osu.Game/Users/UserStatistics.cs
+++ b/osu.Game/Users/UserStatistics.cs
@@ -27,6 +27,9 @@ namespace osu.Game.Users
public int Progress;
}
+ [JsonProperty(@"is_ranked")]
+ public bool IsRanked;
+
[JsonProperty(@"global_rank")]
public int? GlobalRank;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 97bdf00f69..c538f55e89 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index d4b7339900..5cc1857dfe 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -60,7 +60,7 @@
-
+
@@ -83,7 +83,7 @@
-
+