1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-22 03:52:54 +08:00

Merge branch 'master' into refactor-diffcalc

This commit is contained in:
Dan Balasescu 2021-12-22 12:31:28 +09:00 committed by GitHub
commit 98f044881e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1526 additions and 953 deletions

View File

@ -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: 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 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 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. - When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1217.1" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.1221.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -14,6 +14,7 @@ namespace osu.Desktop.Windows
{ {
private Bindable<bool> disableWinKey; private Bindable<bool> disableWinKey;
private IBindable<bool> localUserPlaying; private IBindable<bool> localUserPlaying;
private IBindable<bool> isActive;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; }
@ -24,13 +25,16 @@ namespace osu.Desktop.Windows
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking()); localUserPlaying.BindValueChanged(_ => updateBlocking());
isActive = host.IsActive.GetBoundCopy();
isActive.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey); disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true); disableWinKey.BindValueChanged(_ => updateBlocking(), true);
} }
private void updateBlocking() private void updateBlocking()
{ {
bool shouldDisable = disableWinKey.Value && localUserPlaying.Value; bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value;
if (shouldDisable) if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable); host.InputThread.Scheduler.Add(WindowsKey.Disable);

View File

@ -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; private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0; protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. 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; 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++) for (int i = 0; i < Previous.Count; i++)
{ {
var osuPrevious = (OsuDifficultyHitObject)Previous[i]; var currentObj = (OsuDifficultyHitObject)Previous[i];
var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject); 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. // We want to nerf objects that can be easily seen within the Flashlight circle radius.
if (i == 0) if (i == 0)
smallDistNerf = Math.Min(1.0, jumpDistance / 75.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. // 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); return Math.Pow(smallDistNerf * result, 2.0);

View File

@ -61,6 +61,7 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -94,6 +95,7 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -160,6 +162,7 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -168,7 +171,7 @@ namespace osu.Game.Tests.NonVisual
public void TestMigrationBetweenTwoTargets() public void TestMigrationBetweenTwoTargets()
{ {
string customPath = prepareCustomPath(); string customPath = prepareCustomPath();
string customPath2 = prepareCustomPath("-2"); string customPath2 = prepareCustomPath();
using (var host = new CustomTestHeadlessGameHost()) using (var host = new CustomTestHeadlessGameHost())
{ {
@ -185,7 +188,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(File.Exists(Path.Combine(customPath2, database_filename))); 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. // 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.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename))); Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
@ -193,6 +196,8 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
cleanupPath(customPath2);
} }
} }
} }
@ -214,6 +219,7 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -243,6 +249,7 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -272,6 +279,7 @@ namespace osu.Game.Tests.NonVisual
finally finally
{ {
host.Exit(); host.Exit();
cleanupPath(customPath);
} }
} }
} }
@ -286,14 +294,18 @@ namespace osu.Game.Tests.NonVisual
return path; 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}"); try
{
if (Directory.Exists(path)) if (Directory.Exists(path))
Directory.Delete(path, true); Directory.Delete(path, true);
}
return path; catch
{
}
} }
public class CustomTestHeadlessGameHost : CleanRunHeadlessGameHost public class CustomTestHeadlessGameHost : CleanRunHeadlessGameHost

View File

@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Input; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tests.NonVisual namespace osu.Game.Tests.NonVisual
{ {
@ -11,37 +12,32 @@ namespace osu.Game.Tests.NonVisual
public class SessionStaticsTest public class SessionStaticsTest
{ {
private SessionStatics sessionStatics; private SessionStatics sessionStatics;
private IdleTracker sessionIdleTracker;
[SetUp] [Test]
public void SetUp() public void TestSessionStaticsReset()
{ {
sessionStatics = new SessionStatics(); sessionStatics = new SessionStatics();
sessionIdleTracker = new GameIdleTracker(1000);
sessionStatics.SetValue(Static.LoginOverlayDisplayed, true); sessionStatics.SetValue(Static.LoginOverlayDisplayed, true);
sessionStatics.SetValue(Static.MutedAudioNotificationShownOnce, true); sessionStatics.SetValue(Static.MutedAudioNotificationShownOnce, true);
sessionStatics.SetValue(Static.LowBatteryNotificationShownOnce, true); sessionStatics.SetValue(Static.LowBatteryNotificationShownOnce, true);
sessionStatics.SetValue(Static.LastHoverSoundPlaybackTime, (double?)1d); 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 => Assert.IsFalse(sessionStatics.GetBindable<bool>(Static.LoginOverlayDisplayed).IsDefault);
{ Assert.IsFalse(sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).IsDefault);
if (e.NewValue) Assert.IsFalse(sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce).IsDefault);
sessionStatics.ResetValues(); Assert.IsFalse(sessionStatics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime).IsDefault);
}); Assert.IsFalse(sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds).IsDefault);
}
[Test] sessionStatics.ResetAfterInactivity();
[Timeout(2000)]
public void TestSessionStaticsReset() Assert.IsTrue(sessionStatics.GetBindable<bool>(Static.LoginOverlayDisplayed).IsDefault);
{ Assert.IsTrue(sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).IsDefault);
sessionIdleTracker.IsIdle.BindValueChanged(e => Assert.IsTrue(sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce).IsDefault);
{ // some statics should not reset despite inactivity.
Assert.IsTrue(sessionStatics.GetBindable<bool>(Static.LoginOverlayDisplayed).IsDefault); Assert.IsFalse(sessionStatics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime).IsDefault);
Assert.IsTrue(sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).IsDefault); Assert.IsFalse(sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds).IsDefault);
Assert.IsTrue(sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce).IsDefault);
Assert.IsTrue(sessionStatics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime).IsDefault);
});
} }
} }
} }

View File

@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<PlaylistItem>();
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);
});
}
}
}

View File

@ -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. // 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))); AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); 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); AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);

View File

@ -96,6 +96,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
var longName = CreateAPIBeatmapSet(Ruleset.Value); var longName = CreateAPIBeatmapSet(Ruleset.Value);
longName.Title = longName.TitleUnicode = "this track has an incredibly and implausibly long title"; 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.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.HasExplicitContent = true;
longName.TrackId = 444; longName.TrackId = 444;
@ -251,13 +252,19 @@ namespace osu.Game.Tests.Visual.Beatmaps
[Test] [Test]
public void TestNormal() public void TestNormal()
{ {
createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); createTestCase(beatmapSetInfo => new BeatmapCardNormal(beatmapSetInfo));
}
[Test]
public void TestExtra()
{
createTestCase(beatmapSetInfo => new BeatmapCardExtra(beatmapSetInfo));
} }
[Test] [Test]
public void TestHoverState() 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())); AddStep("Hover card", () => InputManager.MoveMouseTo(firstCard()));
AddWaitStep("wait for potential state change", 5); AddWaitStep("wait for potential state change", 5);
@ -274,10 +281,10 @@ namespace osu.Game.Tests.Visual.Beatmaps
AddWaitStep("wait for potential state change", 5); AddWaitStep("wait for potential state change", 5);
AddAssert("card is still expanded", () => firstCard().Expanded.Value); AddAssert("card is still expanded", () => firstCard().Expanded.Value);
AddStep("Hover away", () => InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapCard>().Last())); AddStep("Hover away", () => InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapCardNormal>().Last()));
AddUntilStep("card is not expanded", () => !firstCard().Expanded.Value); AddUntilStep("card is not expanded", () => !firstCard().Expanded.Value);
BeatmapCard firstCard() => this.ChildrenOfType<BeatmapCard>().First(); BeatmapCardNormal firstCard() => this.ChildrenOfType<BeatmapCardNormal>().First();
} }
} }
} }

View File

@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; 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.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -31,17 +29,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected BeatmapInfo InitialBeatmap { get; private set; } protected BeatmapInfo InitialBeatmap { get; private set; }
protected BeatmapInfo OtherBeatmap { get; private set; } protected BeatmapInfo OtherBeatmap { get; private set; }
protected IScreen CurrentScreen => multiplayerScreenStack.CurrentScreen; protected IScreen CurrentScreen => multiplayerComponents.CurrentScreen;
protected IScreen CurrentSubScreen => multiplayerScreenStack.MultiplayerScreen.CurrentSubScreen; protected IScreen CurrentSubScreen => multiplayerComponents.MultiplayerScreen.CurrentSubScreen;
private BeatmapManager beatmaps; private BeatmapManager beatmaps;
private RulesetStore rulesets; private RulesetStore rulesets;
private BeatmapSetInfo importedSet; private BeatmapSetInfo importedSet;
private TestMultiplayerScreenStack multiplayerScreenStack; private TestMultiplayerComponents multiplayerComponents;
protected TestMultiplayerClient Client => multiplayerScreenStack.Client; protected TestMultiplayerClient Client => multiplayerComponents.Client;
protected TestMultiplayerRoomManager RoomManager => multiplayerScreenStack.RoomManager;
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -65,12 +62,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0); OtherBeatmap = importedSet.Beatmaps.Last(b => b.RulesetID == 0);
}); });
AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack())); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded); AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open(new Room AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open(new Room
{ {
Name = { Value = "Test Room" }, Name = { Value = "Test Room" },
QueueMode = { Value = Mode }, QueueMode = { Value = Mode },
@ -87,13 +84,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2); AddWaitStep("wait for transition", 2);
AddStep("create room", () => ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for join", () => RoomManager.RoomJoined); AddUntilStep("wait for join", () => Client.RoomJoined);
} }
[Test] [Test]
@ -105,24 +98,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected void RunGameplay() protected void RunGameplay()
{ {
AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle); AddUntilStep("wait for idle", () => Client.LocalUser?.State == MultiplayerUserState.Idle);
clickReadyButton(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready); AddUntilStep("wait for ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready);
clickReadyButton(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player player && player.IsLoaded); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded);
AddStep("exit player", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
}
private void clickReadyButton()
{
AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().ChildrenOfType<Button>().Single().Enabled.Value);
AddStep("click ready button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerReadyButton>().Single());
InputManager.Click(MouseButton.Left);
});
} }
} }
} }

View File

@ -46,10 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets; private RulesetStore rulesets;
private BeatmapSetInfo importedSet; private BeatmapSetInfo importedSet;
private TestMultiplayerScreenStack multiplayerScreenStack; private TestMultiplayerComponents multiplayerComponents;
private TestMultiplayerClient client => multiplayerScreenStack.Client; private TestMultiplayerClient client => multiplayerComponents.Client;
private TestMultiplayerRoomManager roomManager => multiplayerScreenStack.RoomManager; private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -71,8 +71,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
}); });
AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack())); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded); AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
} }
@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Press select", () => InputManager.Key(Key.Enter)); AddStep("Press select", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => roomManager.RoomJoined); AddUntilStep("wait for join", () => client.RoomJoined);
} }
[Test] [Test]
@ -295,7 +295,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join room", () => InputManager.Key(Key.Enter)); AddStep("join room", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => roomManager.RoomJoined); AddUntilStep("wait for join", () => client.RoomJoined);
AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1); AddAssert("Check participant count correct", () => client.APIRoom?.ParticipantCount.Value == 1);
AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1); AddAssert("Check participant list contains user", () => client.APIRoom?.RecentParticipants.Count(u => u.Id == API.LocalUser.Value.Id) == 1);
@ -353,7 +353,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick()); AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => roomManager.RoomJoined); AddUntilStep("wait for join", () => client.RoomJoined);
} }
[Test] [Test]
@ -419,7 +419,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () => AddStep("Enter song select", () =>
{ {
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen; var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId); ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId);
}); });
@ -433,7 +433,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("start match externally", () => client.StartMatch()); AddStep("start match externally", () => client.StartMatch());
AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is Player); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID); AddAssert("Beatmap matches current item", () => Beatmap.Value.BeatmapInfo.OnlineID == client.Room?.Playlist.First().BeatmapID);
} }
@ -463,17 +463,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddStep("click spectate button", () => ClickButtonWhenEnabled<MultiplayerSpectateButton>();
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => client.StartMatch()); AddStep("start match externally", () => client.StartMatch());
AddAssert("play not started", () => multiplayerScreenStack.IsCurrentScreen()); AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
} }
[Test] [Test]
@ -501,11 +497,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready); client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready);
}); });
AddStep("click spectate button", () => ClickButtonWhenEnabled<MultiplayerSpectateButton>();
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating); AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
@ -517,7 +509,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
}); });
AddUntilStep("play started", () => multiplayerScreenStack.CurrentScreen is SpectatorScreen); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is SpectatorScreen);
} }
[Test] [Test]
@ -559,16 +551,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("open mod overlay", () => this.ChildrenOfType<RoomSubScreen.UserModSelectButton>().Single().TriggerClick()); AddStep("open mod overlay", () => this.ChildrenOfType<RoomSubScreen.UserModSelectButton>().Single().TriggerClick());
AddStep("invoke on back button", () => multiplayerScreenStack.OnBackButton()); AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden); AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
testLeave("back button", () => multiplayerScreenStack.OnBackButton()); testLeave("back button", () => multiplayerComponents.OnBackButton());
// mimics home button and OS window close // mimics home button and OS window close
testLeave("forced exit", () => multiplayerScreenStack.Exit()); testLeave("forced exit", () => multiplayerComponents.Exit());
void testLeave(string actionName, Action action) void testLeave(string actionName, Action action)
{ {
@ -605,7 +597,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.GameplayClock.CurrentTime > time); AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.GameplayClock.CurrentTime > time);
} }
AddUntilStep("wait for results", () => multiplayerScreenStack.CurrentScreen is ResultsScreen); AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
} }
[Test] [Test]
@ -646,7 +638,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join room", () => InputManager.Key(Key.Enter)); AddStep("join room", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for join", () => roomManager.RoomJoined); AddUntilStep("wait for join", () => client.RoomJoined);
AddAssert("local room has correct settings", () => AddAssert("local room has correct settings", () =>
{ {
@ -680,15 +672,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
pressReadyButton(1234); pressReadyButton(1234);
AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
AddStep("press back button and exit", () => AddStep("press back button and exit", () =>
{ {
multiplayerScreenStack.OnBackButton(); multiplayerComponents.OnBackButton();
multiplayerScreenStack.Exit(); multiplayerComponents.Exit();
}); });
AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen()); AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen());
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle); AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
} }
@ -716,17 +708,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready)); AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
pressReadyButton(1234); pressReadyButton(1234);
AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true); AddUntilStep("wait for gameplay", () => (multiplayerComponents.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded)); AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded));
AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay)); AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay));
AddStep("press back button and exit", () => AddStep("press back button and exit", () =>
{ {
multiplayerScreenStack.OnBackButton(); multiplayerComponents.OnBackButton();
multiplayerScreenStack.Exit(); multiplayerComponents.Exit();
}); });
AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen()); AddUntilStep("wait for return to match subscreen", () => multiplayerComponents.MultiplayerScreen.IsCurrentScreen());
AddWaitStep("wait for possible state change", 5); AddWaitStep("wait for possible state change", 5);
AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating); AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
} }
@ -758,7 +750,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2); AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
AddUntilStep("queue contains item", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Single().ID == 2); AddUntilStep("queue contains item", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Single().ID == 2);
} }
@ -792,7 +784,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2)); AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2));
AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1); AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1);
AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent()); AddStep("exit gameplay as initial user", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
AddUntilStep("queue is empty", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Count == 0); AddUntilStep("queue is empty", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Count == 0);
} }
@ -800,13 +792,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
pressReadyButton(); pressReadyButton();
pressReadyButton(); pressReadyButton();
AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player); AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player);
} }
private ReadyButton readyButton => this.ChildrenOfType<ReadyButton>().Single(); private ReadyButton readyButton => this.ChildrenOfType<ReadyButton>().Single();
private void pressReadyButton(int? playingUserId = null) private void pressReadyButton(int? playingUserId = null)
{ {
// Can't use ClickButtonWhenEnabled<> due to needing to store the state after the button is enabled.
AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value); AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value);
MultiplayerUserState lastState = MultiplayerUserState.Idle; MultiplayerUserState lastState = MultiplayerUserState.Idle;
@ -826,19 +820,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createRoom(Func<Room> room) private void createRoom(Func<Room> room)
{ {
AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open(room())); AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2); AddWaitStep("wait for transition", 2);
AddStep("create room", () => ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for join", () => roomManager.RoomJoined); AddUntilStep("wait for join", () => client.RoomJoined);
} }
} }
} }

View File

@ -14,12 +14,10 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -62,22 +60,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestCreatedRoom() public void TestCreatedRoom()
{ {
AddStep("create room", () => AddStep("add playlist item", () =>
{ {
SelectedRoom.Value.Playlist.Add(new PlaylistItem SelectedRoom.Value.Playlist.Add(new PlaylistItem
{ {
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo },
}); });
// Needs to run after components update with the playlist item.
Schedule(() =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
}); });
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined); AddUntilStep("wait for join", () => RoomJoined);
} }
@ -110,11 +103,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
}); });
AddStep("click create button", () => ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for room join", () => RoomJoined); AddUntilStep("wait for room join", () => RoomJoined);
@ -124,21 +113,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready); Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready);
}); });
AddStep("click spectate button", () => ClickButtonWhenEnabled<MultiplayerSpectateButton>();
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for spectating user state", () => Client.LocalUser?.State == MultiplayerUserState.Spectating); AddUntilStep("wait for spectating user state", () => Client.LocalUser?.State == MultiplayerUserState.Spectating);
AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().ChildrenOfType<ReadyButton>().Single().Enabled.Value); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("click ready button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerReadyButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
} }

View File

@ -13,7 +13,6 @@ using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@ -22,7 +21,6 @@ using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist; using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -142,21 +140,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("item arrived in playlist", () => playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().Any(i => i.Model.ID == itemId)); AddUntilStep("item arrived in playlist", () => playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().Any(i => i.Model.ID == itemId));
} }
private void deleteItem(int index)
{
OsuRearrangeableListItem<PlaylistItem> item = null;
AddStep($"move mouse to delete button {index}", () =>
{
item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddUntilStep("item removed from playlist", () => !playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().Contains(item));
}
private void assertDeleteButtonVisibility(int index, bool visible) private void assertDeleteButtonVisibility(int index, bool visible)
=> AddUntilStep($"delete button {index} {(visible ? "is" : "is not")} visible", => AddUntilStep($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(index).Alpha > 0) == visible); () => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(index).Alpha > 0) == visible);

View File

@ -9,7 +9,6 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -22,7 +21,6 @@ using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -113,10 +111,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.TransferHost(2); Client.TransferHost(2);
}); });
addClickButtonStep(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
} }
@ -132,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); Client.AddUser(new APIUser { Id = 2, Username = "Another user" });
}); });
addClickButtonStep(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
verifyGameplayStartFlow(); verifyGameplayStartFlow();
@ -147,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.TransferHost(2); Client.TransferHost(2);
}); });
addClickButtonStep(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0));
verifyGameplayStartFlow(); verifyGameplayStartFlow();
@ -162,11 +160,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.AddUser(new APIUser { Id = 2, Username = "Another user" }); Client.AddUser(new APIUser { Id = 2, Username = "Another user" });
}); });
addClickButtonStep(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0)); AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0));
addClickButtonStep(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); AddUntilStep("user is idle (match not started)", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
} }
[TestCase(true)] [TestCase(true)]
@ -184,7 +185,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
if (!isHost) if (!isHost)
AddStep("transfer host", () => Client.TransferHost(2)); AddStep("transfer host", () => Client.TransferHost(2));
addClickButtonStep(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () => AddRepeatStep("change user ready state", () =>
{ {
@ -199,20 +200,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
}, users); }, users);
} }
private void addClickButtonStep()
{
AddUntilStep("wait for button to be ready", () => button.ChildrenOfType<Button>().Single().Enabled.Value);
AddStep("click button", () =>
{
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
}
private void verifyGameplayStartFlow() private void verifyGameplayStartFlow()
{ {
AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); AddUntilStep("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddUntilStep("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value); AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value);

View File

@ -21,7 +21,6 @@ using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -121,10 +120,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(MultiplayerUserState.Ready)] [TestCase(MultiplayerUserState.Ready)]
public void TestToggleWhenIdle(MultiplayerUserState initialState) public void TestToggleWhenIdle(MultiplayerUserState initialState)
{ {
addClickSpectateButtonStep(); ClickButtonWhenEnabled<MultiplayerSpectateButton>();
AddUntilStep("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating); AddUntilStep("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating);
addClickSpectateButtonStep(); ClickButtonWhenEnabled<MultiplayerSpectateButton>();
AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); AddUntilStep("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
} }
@ -138,7 +137,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestReadyButtonDisabledWhenHostAndNoReadyUsers() public void TestReadyButtonDisabledWhenHostAndNoReadyUsers()
{ {
addClickSpectateButtonStep(); ClickButtonWhenEnabled<MultiplayerSpectateButton>();
assertReadyButtonEnablement(false); assertReadyButtonEnablement(false);
} }
@ -148,7 +147,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add user", () => Client.AddUser(new APIUser { Id = PLAYER_1_ID })); AddStep("add user", () => Client.AddUser(new APIUser { Id = PLAYER_1_ID }));
AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
addClickSpectateButtonStep(); ClickButtonWhenEnabled<MultiplayerSpectateButton>();
assertReadyButtonEnablement(true); assertReadyButtonEnablement(true);
} }
@ -163,16 +162,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready)); AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
addClickSpectateButtonStep(); ClickButtonWhenEnabled<MultiplayerSpectateButton>();
assertReadyButtonEnablement(false); assertReadyButtonEnablement(false);
} }
private void addClickSpectateButtonStep() => AddStep("click spectate button", () =>
{
InputManager.MoveMouseTo(spectateButton);
InputManager.Click(MouseButton.Left);
});
private void assertSpectateButtonEnablement(bool shouldBeEnabled) private void assertSpectateButtonEnablement(bool shouldBeEnabled)
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled); => AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);

View File

@ -31,9 +31,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private RulesetStore rulesets; private RulesetStore rulesets;
private BeatmapSetInfo importedSet; private BeatmapSetInfo importedSet;
private TestMultiplayerScreenStack multiplayerScreenStack; private TestMultiplayerComponents multiplayerComponents;
private TestMultiplayerClient client => multiplayerScreenStack.Client; private TestMultiplayerClient client => multiplayerComponents.Client;
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -55,8 +55,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
}); });
AddStep("load multiplayer", () => LoadScreen(multiplayerScreenStack = new TestMultiplayerScreenStack())); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreenStack.IsLoaded); AddUntilStep("wait for multiplayer to load", () => multiplayerComponents.IsLoaded);
AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge to load", () => this.ChildrenOfType<MultiplayerLoungeSubScreen>().FirstOrDefault()?.IsLoaded == true);
} }
@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press own button", () => AddStep("press own button", () =>
{ {
InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().First()); InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType<TeamDisplay>().First());
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("press other user's button", () => AddStep("press other user's button", () =>
{ {
InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().ElementAt(1)); InputManager.MoveMouseTo(multiplayerComponents.ChildrenOfType<TeamDisplay>().ElementAt(1));
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
@ -164,18 +164,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam == null)); AddUntilStep("team displays are not displaying teams", () => multiplayerComponents.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam == null));
AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus)); AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus));
AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam != null)); AddUntilStep("team displays are displaying teams", () => multiplayerComponents.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam != null));
} }
private void createRoom(Func<Room> room) private void createRoom(Func<Room> room)
{ {
AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open(room())); AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2); AddWaitStep("wait for transition", 2);
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("wait for join", () => multiplayerScreenStack.RoomManager.RoomJoined); AddUntilStep("wait for join", () => client.RoomJoined);
} }
} }
} }

View File

@ -336,12 +336,12 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestPushMatchSubScreenAndPressBackButtonImmediately() public void TestPushMatchSubScreenAndPressBackButtonImmediately()
{ {
TestMultiplayerScreenStack multiplayerScreenStack = null; TestMultiplayerComponents multiplayerComponents = null;
PushAndConfirm(() => multiplayerScreenStack = new TestMultiplayerScreenStack()); PushAndConfirm(() => multiplayerComponents = new TestMultiplayerComponents());
AddUntilStep("wait for lounge", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerScreenStack.ChildrenOfType<LoungeSubScreen>().Single().Open()); AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2); AddWaitStep("wait two frames", 2);
} }

View File

@ -127,6 +127,7 @@ namespace osu.Game.Tests.Visual.Online
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults() public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults()
{ {
AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("fetch for 0 beatmaps", () => fetchFor());
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
// only Rank Achieved filter // only Rank Achieved filter
@ -187,6 +188,9 @@ namespace osu.Game.Tests.Visual.Online
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults() public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults()
{ {
AddStep("fetch for 1 beatmap", () => fetchFor(CreateAPIBeatmapSet(Ruleset.Value))); AddStep("fetch for 1 beatmap", () => fetchFor(CreateAPIBeatmapSet(Ruleset.Value)));
noPlaceholderShown();
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false); AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
// only Rank Achieved filter // only Rank Achieved filter
@ -217,6 +221,9 @@ namespace osu.Game.Tests.Visual.Online
public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults() public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults()
{ {
AddStep("fetch for 1 beatmap", () => fetchFor(CreateAPIBeatmapSet(Ruleset.Value))); AddStep("fetch for 1 beatmap", () => fetchFor(CreateAPIBeatmapSet(Ruleset.Value)));
noPlaceholderShown();
AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true); AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
// only Rank Achieved filter // only Rank Achieved filter
@ -280,9 +287,8 @@ namespace osu.Game.Tests.Visual.Online
private void noPlaceholderShown() private void noPlaceholderShown()
{ {
AddUntilStep("no placeholder shown", () => AddUntilStep("\"supporter required\" placeholder not shown", () => !overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any(d => d.IsPresent));
!overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any(d => d.IsPresent) AddUntilStep("\"no maps found\" placeholder not shown", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
&& !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
} }
} }
} }

View File

@ -2,9 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Online.API; using osu.Framework.Testing;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Profile; using osu.Game.Overlays.Profile;
@ -14,72 +15,77 @@ namespace osu.Game.Tests.Visual.Online
{ {
public class TestSceneUserProfileHeader : OsuTestScene public class TestSceneUserProfileHeader : OsuTestScene
{ {
protected override bool UseOnlineAPI => true;
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Resolved] private ProfileHeader header;
private IAPIProvider api { get; set; }
private readonly ProfileHeader header; [SetUpSteps]
public void SetUpSteps()
public TestSceneUserProfileHeader()
{ {
header = new ProfileHeader(); AddStep("create header", () => Child = header = new ProfileHeader());
Add(header); }
AddStep("Show test dummy", () => header.User.Value = TestSceneUserProfileOverlay.TEST_USER); [Test]
public void TestBasic()
{
AddStep("Show example user", () => header.User.Value = TestSceneUserProfileOverlay.TEST_USER);
}
AddStep("Show null dummy", () => header.User.Value = new APIUser [Test]
{ public void TestOnlineState()
Username = "Null" {
}); AddStep("Show online user", () => header.User.Value = new APIUser
AddStep("Show online dummy", () => header.User.Value = new APIUser
{ {
Id = 1001,
Username = "IAmOnline", Username = "IAmOnline",
LastVisit = DateTimeOffset.Now, LastVisit = DateTimeOffset.Now,
IsOnline = true, IsOnline = true,
}); });
AddStep("Show offline dummy", () => header.User.Value = new APIUser AddStep("Show offline user", () => header.User.Value = new APIUser
{ {
Id = 1002,
Username = "IAmOffline", Username = "IAmOffline",
LastVisit = DateTimeOffset.Now, LastVisit = DateTimeOffset.Now.AddDays(-10),
IsOnline = false, IsOnline = false,
}); });
addOnlineStep("Show ppy", new APIUser
{
Username = @"peppy",
Id = 2,
IsSupporter = true,
Country = new Country { FullName = @"Australia", FlagName = @"AU" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg"
});
addOnlineStep("Show flyte", new APIUser
{
Username = @"flyte",
Id = 3103765,
Country = new Country { FullName = @"Japan", FlagName = @"JP" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
});
} }
private void addOnlineStep(string name, APIUser fallback) [Test]
public void TestRankedState()
{ {
AddStep(name, () => AddStep("Show ranked user", () => header.User.Value = new APIUser
{ {
if (api.IsLoggedIn) Id = 2001,
Username = "RankedUser",
Statistics = new UserStatistics
{ {
var request = new GetUserRequest(fallback.Id); IsRanked = true,
request.Success += user => header.User.Value = user; GlobalRank = 15000,
api.Queue(request); CountryRank = 1500,
RankHistory = new APIRankHistory
{
Mode = @"osu",
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
},
}
});
AddStep("Show unranked user", () => header.User.Value = new APIUser
{
Id = 2002,
Username = "UnrankedUser",
Statistics = new UserStatistics
{
IsRanked = false,
// web will sometimes return non-empty rank history even for unranked users.
RankHistory = new APIRankHistory
{
Mode = @"osu",
Data = Enumerable.Range(2345, 85).ToArray()
},
} }
else
header.User.Value = fallback;
}); });
} }
} }

View File

@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Online
ProfileOrder = new[] { "me" }, ProfileOrder = new[] { "me" },
Statistics = new UserStatistics Statistics = new UserStatistics
{ {
IsRanked = true,
GlobalRank = 2148, GlobalRank = 2148,
CountryRank = 1, CountryRank = 1,
PP = 4567.89m, PP = 4567.89m,

View File

@ -168,12 +168,13 @@ namespace osu.Game.Tests.Visual.Playlists
})); }));
}); });
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
waitForDisplay(); waitForDisplay();
} }
private void waitForDisplay() private void waitForDisplay()
{ {
AddUntilStep("wait for load to complete", () => AddUntilStep("wait for scores loaded", () =>
requestComplete requestComplete
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible); && resultsScreen.ScorePanelList.AllPanelsVisible);

View File

@ -203,8 +203,12 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
DelayedFetchResultsScreen screen = null; DelayedFetchResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), 3000))); var tcs = new TaskCompletionSource();
AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), tcs.Task)));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddStep("click expanded panel", () => AddStep("click expanded panel", () =>
{ {
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded); var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
@ -212,6 +216,10 @@ namespace osu.Game.Tests.Visual.Ranking
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("no fetch yet", () => !screen.FetchCompleted);
AddStep("allow fetch", () => tcs.SetResult());
AddUntilStep("wait for fetch", () => screen.FetchCompleted); AddUntilStep("wait for fetch", () => screen.FetchCompleted);
AddAssert("expanded panel still on screen", () => this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0); AddAssert("expanded panel still on screen", () => this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0);
} }
@ -295,21 +303,21 @@ namespace osu.Game.Tests.Visual.Ranking
private class DelayedFetchResultsScreen : TestResultsScreen private class DelayedFetchResultsScreen : TestResultsScreen
{ {
private readonly Task fetchWaitTask;
public bool FetchCompleted { get; private set; } public bool FetchCompleted { get; private set; }
private readonly double delay; public DelayedFetchResultsScreen(ScoreInfo score, Task fetchWaitTask = null)
public DelayedFetchResultsScreen(ScoreInfo score, double delay)
: base(score) : base(score)
{ {
this.delay = delay; this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask;
} }
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{ {
Task.Run(async () => Task.Run(async () =>
{ {
await Task.Delay(TimeSpan.FromMilliseconds(delay)); await fetchWaitTask;
var scores = new List<ScoreInfo>(); var scores = new List<ScoreInfo>();

View File

@ -466,7 +466,9 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestExternalBeatmapChangeWhileFiltered(bool differentRuleset) public void TestExternalBeatmapChangeWhileFiltered(bool differentRuleset)
{ {
createSongSelect(); createSongSelect();
addManyTestMaps(); // ensure there is at least 1 difficulty for each of the rulesets
// (catch is excluded inside of addManyTestMaps).
addManyTestMaps(3);
changeRuleset(0); changeRuleset(0);
@ -488,8 +490,9 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select beatmap externally", () => AddStep("select beatmap externally", () =>
{ {
target = manager.GetAllUsableBeatmapSets() target = manager.GetAllUsableBeatmapSets()
.Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) .First(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset))
.ElementAt(5).Beatmaps.First(bi => bi.RulesetID == targetRuleset); .Beatmaps
.First(bi => bi.RulesetID == targetRuleset);
Beatmap.Value = manager.GetWorkingBeatmap(target); Beatmap.Value = manager.GetWorkingBeatmap(target);
}); });
@ -518,7 +521,9 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestExternalBeatmapChangeWhileFilteredThenRefilter() public void TestExternalBeatmapChangeWhileFilteredThenRefilter()
{ {
createSongSelect(); createSongSelect();
addManyTestMaps(); // ensure there is at least 1 difficulty for each of the rulesets
// (catch is excluded inside of addManyTestMaps).
addManyTestMaps(3);
changeRuleset(0); changeRuleset(0);
@ -534,8 +539,10 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select beatmap externally", () => AddStep("select beatmap externally", () =>
{ {
target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == 1)) target = manager
.ElementAt(5).Beatmaps.First(); .GetAllUsableBeatmapSets()
.First(b => b.Beatmaps.Any(bi => bi.RulesetID == 1))
.Beatmaps.First();
Beatmap.Value = manager.GetWorkingBeatmap(target); Beatmap.Value = manager.GetWorkingBeatmap(target);
}); });
@ -877,14 +884,21 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for carousel loaded", () => songSelect.Carousel.IsAlive); AddUntilStep("wait for carousel loaded", () => songSelect.Carousel.IsAlive);
} }
private void addManyTestMaps() /// <summary>
/// Imports test beatmap sets to show in the carousel.
/// </summary>
/// <param name="difficultyCountPerSet">
/// The exact count of difficulties to create for each beatmap set.
/// A <see langword="null"/> value causes the count of difficulties to be selected randomly.
/// </param>
private void addManyTestMaps(int? difficultyCountPerSet = null)
{ {
AddStep("import test maps", () => AddStep("import test maps", () =>
{ {
var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray(); var usableRulesets = rulesets.AvailableRulesets.Where(r => r.OnlineID != 2).ToArray();
for (int i = 0; i < 100; i += 10) for (int i = 0; i < 10; i++)
manager.Import(TestResources.CreateTestBeatmapSetInfo(rulesets: usableRulesets)).Wait(); manager.Import(TestResources.CreateTestBeatmapSetInfo(difficultyCountPerSet, usableRulesets)).Wait();
}); });
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual
/// </list> /// </list>
/// </p> /// </p>
/// </summary> /// </summary>
public class TestMultiplayerScreenStack : OsuScreen public class TestMultiplayerComponents : OsuScreen
{ {
public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen; public Screens.OnlinePlay.Multiplayer.Multiplayer MultiplayerScreen => multiplayerScreen;
@ -42,14 +42,18 @@ namespace osu.Game.Tests.Visual
private readonly OsuScreenStack screenStack; private readonly OsuScreenStack screenStack;
private readonly TestMultiplayer multiplayerScreen; private readonly TestMultiplayer multiplayerScreen;
public TestMultiplayerScreenStack() public TestMultiplayerComponents()
{ {
multiplayerScreen = new TestMultiplayer(); multiplayerScreen = new TestMultiplayer();
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
Client = new TestMultiplayerClient(RoomManager), Client = new TestMultiplayerClient(RoomManager),
screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both } screenStack = new OsuScreenStack
{
Name = nameof(TestMultiplayerComponents),
RelativeSizeAxes = Axes.Both
}
}; };
screenStack.Push(multiplayerScreen); screenStack.Push(multiplayerScreen);

View File

@ -3,431 +3,78 @@
#nullable enable #nullable enable
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; 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.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.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; 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 namespace osu.Game.Beatmaps.Drawables.Cards
{ {
public class BeatmapCard : OsuClickableContainer public abstract class BeatmapCard : OsuClickableContainer
{ {
public const float TRANSITION_DURATION = 400; public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10; public const float CORNER_RADIUS = 10;
public IBindable<bool> Expanded { get; } public IBindable<bool> Expanded { get; }
private const float width = 408; protected readonly APIBeatmapSet BeatmapSet;
private const float height = 100; protected readonly Bindable<BeatmapSetFavouriteState> FavouriteState;
private const float icon_area_width = 30;
private readonly APIBeatmapSet beatmapSet; protected abstract Drawable IdleContent { get; }
private readonly Bindable<BeatmapSetFavouriteState> favouriteState; protected abstract Drawable DownloadInProgressContent { get; }
private readonly BeatmapDownloadTracker downloadTracker; protected readonly BeatmapDownloadTracker DownloadTracker;
private BeatmapCardContent content = null!; protected BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
private BeatmapCardThumbnail thumbnail = null!;
private Container rightAreaBackground = null!;
private Container<BeatmapCardIconButton> rightAreaButtons = null!;
private Container mainContent = null!;
private BeatmapCardContentBackground mainContentBackground = null!;
private FillFlowContainer<BeatmapCardStatistic> 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)
: base(HoverSampleSet.Submit) : base(HoverSampleSet.Submit)
{ {
Expanded = new BindableBool { Disabled = !allowExpansion }; Expanded = new BindableBool { Disabled = !allowExpansion };
this.beatmapSet = beatmapSet; BeatmapSet = beatmapSet;
favouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount)); FavouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
downloadTracker = new BeatmapDownloadTracker(beatmapSet); DownloadTracker = new BeatmapDownloadTracker(beatmapSet);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(BeatmapSetOverlay? beatmapSetOverlay) private void load(BeatmapSetOverlay? beatmapSetOverlay)
{ {
Width = width; Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
Height = height;
FillFlowContainer leftIconArea; AddInternal(DownloadTracker);
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<BeatmapCardIconButton>
{
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<BeatmapCardStatistic>
{
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);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
downloadTracker.State.BindValueChanged(_ => updateState()); DownloadTracker.State.BindValueChanged(_ => UpdateState());
Expanded.BindValueChanged(_ => updateState(), true); Expanded.BindValueChanged(_ => UpdateState(), true);
FinishTransforms(true); FinishTransforms(true);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
updateState(); UpdateState();
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
updateState(); UpdateState();
base.OnHoverLost(e); base.OnHoverLost(e);
} }
private LocalisableString createArtistText() protected virtual void UpdateState()
{ {
var romanisableArtist = new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist); bool showProgress = DownloadTracker.State.Value == DownloadState.Downloading || DownloadTracker.State.Value == DownloadState.Importing;
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
}
private IEnumerable<BeatmapCardStatistic> createStatistics() IdleContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint);
{ DownloadInProgressContent.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
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);
} }
} }
} }

View File

@ -139,6 +139,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private void updateState() 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); background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
dropdownContent.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); borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);

View File

@ -0,0 +1,317 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}
}

View File

@ -1,21 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards 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) public BeatmapCardExtraInfoRow(APIBeatmapSet beatmapSet)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Child = new FillFlowContainer InternalChild = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, 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);
}
} }
} }

View File

@ -0,0 +1,286 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<BeatmapCardStatistic> 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<BeatmapCardStatistic>
{
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<BeatmapCardStatistic> 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);
}
}
}

View File

@ -0,0 +1,184 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<bool> ShowDetails = new Bindable<bool>();
public Bindable<BeatmapSetFavouriteState> FavouriteState = new Bindable<BeatmapSetFavouriteState>();
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<Drawable> Content => mainContent;
private readonly Container background;
private readonly Container buttonArea;
private readonly Container<BeatmapCardIconButton> 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<BeatmapCardIconButton>
{
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;
}
}
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
@ -12,11 +14,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
/// </summary> /// </summary>
public class HypesStatistic : BeatmapCardStatistic public class HypesStatistic : BeatmapCardStatistic
{ {
public HypesStatistic(BeatmapSetHypeStatus hypeStatus) private HypesStatistic(BeatmapSetHypeStatus hypeStatus)
{ {
Icon = FontAwesome.Solid.Bullhorn; Icon = FontAwesome.Solid.Bullhorn;
Text = hypeStatus.Current.ToLocalisableString(); Text = hypeStatus.Current.ToLocalisableString();
TooltipText = BeatmapsStrings.HypeRequiredText(hypeStatus.Current.ToLocalisableString(), hypeStatus.Required.ToLocalisableString()); TooltipText = BeatmapsStrings.HypeRequiredText(hypeStatus.Current.ToLocalisableString(), hypeStatus.Required.ToLocalisableString());
} }
public static HypesStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetOnlineInfo)
=> beatmapSetOnlineInfo.HypeStatus == null ? null : new HypesStatistic(beatmapSetOnlineInfo.HypeStatus);
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
@ -12,11 +14,16 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
/// </summary> /// </summary>
public class NominationsStatistic : BeatmapCardStatistic public class NominationsStatistic : BeatmapCardStatistic
{ {
public NominationsStatistic(BeatmapSetNominationStatus nominationStatus) private NominationsStatistic(BeatmapSetNominationStatus nominationStatus)
{ {
Icon = FontAwesome.Solid.ThumbsUp; Icon = FontAwesome.Solid.ThumbsUp;
Text = nominationStatus.Current.ToLocalisableString(); Text = nominationStatus.Current.ToLocalisableString();
TooltipText = BeatmapsStrings.NominationsRequiredText(nominationStatus.Current.ToLocalisableString(), nominationStatus.Required.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);
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -13,18 +12,27 @@ namespace osu.Game.Configuration
/// </summary> /// </summary>
public class SessionStatics : InMemoryConfigManager<Static> public class SessionStatics : InMemoryConfigManager<Static>
{ {
protected override void InitialiseDefaults() => ResetValues(); protected override void InitialiseDefaults()
public void ResetValues()
{ {
ensureDefault(SetDefault(Static.LoginOverlayDisplayed, false)); SetDefault(Static.LoginOverlayDisplayed, false);
ensureDefault(SetDefault(Static.MutedAudioNotificationShownOnce, false)); SetDefault(Static.MutedAudioNotificationShownOnce, false);
ensureDefault(SetDefault(Static.LowBatteryNotificationShownOnce, false)); SetDefault(Static.LowBatteryNotificationShownOnce, false);
ensureDefault(SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null)); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
ensureDefault(SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null)); SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
} }
private void ensureDefault<T>(Bindable<T> bindable) => bindable.SetDefault(); /// <summary>
/// Revert statics to their defaults after being idle for appropriate amount of time.
/// </summary>
/// <remarks>
/// This only affects a subset of statics which the user would expect to have reset after a break.
/// </remarks>
public void ResetAfterInactivity()
{
GetBindable<bool>(Static.LoginOverlayDisplayed).SetDefault();
GetBindable<bool>(Static.MutedAudioNotificationShownOnce).SetDefault();
GetBindable<bool>(Static.LowBatteryNotificationShownOnce).SetDefault();
}
} }
public enum Static public enum Static

View File

@ -6,20 +6,13 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Database namespace osu.Game.Database
{ {
// This class is based on `UserLookupCache` which is well tested. public class BeatmapLookupCache : OnlineLookupCache<int, APIBeatmap, GetBeatmapsRequest>
// If modifications are to be made here, a base abstract implementation should likely be created and shared between the two.
public class BeatmapLookupCache : MemoryCachingComponent<int, APIBeatmap>
{ {
[Resolved]
private IAPIProvider api { get; set; }
/// <summary> /// <summary>
/// Perform an API lookup on the specified beatmap, populating a <see cref="APIBeatmap"/> model. /// Perform an API lookup on the specified beatmap, populating a <see cref="APIBeatmap"/> model.
/// </summary> /// </summary>
@ -27,7 +20,7 @@ namespace osu.Game.Database
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.</returns> /// <returns>The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull] [ItemCanBeNull]
public Task<APIBeatmap> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => GetAsync(beatmapId, token); public Task<APIBeatmap> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
/// <summary> /// <summary>
/// Perform an API lookup on the specified beatmaps, populating a <see cref="APIBeatmap"/> model. /// Perform an API lookup on the specified beatmaps, populating a <see cref="APIBeatmap"/> model.
@ -35,115 +28,10 @@ namespace osu.Game.Database
/// <param name="beatmapIds">The beatmaps to lookup.</param> /// <param name="beatmapIds">The beatmaps to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated beatmaps. May include null results for failed retrievals.</returns> /// <returns>The populated beatmaps. May include null results for failed retrievals.</returns>
public Task<APIBeatmap[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) public Task<APIBeatmap[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
{
var beatmapLookupTasks = new List<Task<APIBeatmap>>();
foreach (int u in beatmapIds) protected override GetBeatmapsRequest CreateRequest(IEnumerable<int> ids) => new GetBeatmapsRequest(ids.ToArray());
{
beatmapLookupTasks.Add(GetBeatmapAsync(u, token).ContinueWith(task =>
{
if (!task.IsCompletedSuccessfully)
return null;
return task.Result; protected override IEnumerable<APIBeatmap> RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
}, token));
}
return Task.WhenAll(beatmapLookupTasks);
}
protected override async Task<APIBeatmap> ComputeValueAsync(int lookup, CancellationToken token = default)
=> await queryBeatmap(lookup).ConfigureAwait(false);
private readonly Queue<(int id, TaskCompletionSource<APIBeatmap>)> pendingBeatmapTasks = new Queue<(int, TaskCompletionSource<APIBeatmap>)>();
private Task pendingRequestTask;
private readonly object taskAssignmentLock = new object();
private Task<APIBeatmap> queryBeatmap(int beatmapId)
{
lock (taskAssignmentLock)
{
var tcs = new TaskCompletionSource<APIBeatmap>();
// 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<int, List<TaskCompletionSource<APIBeatmap>>>();
// Grab at most 50 unique beatmap IDs from the queue.
lock (taskAssignmentLock)
{
while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 50)
{
(int id, TaskCompletionSource<APIBeatmap> 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<TaskCompletionSource<APIBeatmap>> { 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<APIBeatmap> 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);
} }
} }

View File

@ -0,0 +1,170 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<TLookup, TValue, TRequest> : MemoryCachingComponent<TLookup, TValue>
where TLookup : IEquatable<TLookup>
where TValue : class, IHasOnlineID<TLookup>
where TRequest : APIRequest
{
[Resolved]
private IAPIProvider api { get; set; }
/// <summary>
/// Creates an <see cref="APIRequest"/> to retrieve the values for a given collection of <typeparamref name="TLookup"/>s.
/// </summary>
/// <param name="ids">The IDs to perform the lookup with.</param>
protected abstract TRequest CreateRequest(IEnumerable<TLookup> ids);
/// <summary>
/// Retrieves a list of <typeparamref name="TValue"/>s from a successful <typeparamref name="TRequest"/> created by <see cref="CreateRequest"/>.
/// </summary>
[CanBeNull]
protected abstract IEnumerable<TValue> RetrieveResults(TRequest request);
/// <summary>
/// Perform a lookup using the specified <paramref name="id"/>, populating a <typeparamref name="TValue"/>.
/// </summary>
/// <param name="id">The ID to lookup.</param>
/// <param name="token">An optional cancellation token.</param>
/// <returns>The populated <typeparamref name="TValue"/>, or null if the value does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull]
protected Task<TValue> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
/// <summary>
/// Perform an API lookup on the specified <paramref name="ids"/>, populating a <typeparamref name="TValue"/>.
/// </summary>
/// <param name="ids">The IDs to lookup.</param>
/// <param name="token">An optional cancellation token.</param>
/// <returns>The populated values. May include null results for failed retrievals.</returns>
protected Task<TValue[]> LookupAsync(TLookup[] ids, CancellationToken token = default)
{
var lookupTasks = new List<Task<TValue>>();
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<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default)
=> await queryValue(lookup).ConfigureAwait(false);
private readonly Queue<(TLookup id, TaskCompletionSource<TValue>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue>)>();
private Task pendingRequestTask;
private readonly object taskAssignmentLock = new object();
private Task<TValue> queryValue(TLookup id)
{
lock (taskAssignmentLock)
{
var tcs = new TaskCompletionSource<TValue>();
// 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<TLookup, List<TaskCompletionSource<TValue>>>();
// Grab at most 50 unique IDs from the queue.
lock (taskAssignmentLock)
{
while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50)
{
(TLookup id, TaskCompletionSource<TValue> 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<TaskCompletionSource<TValue>> { 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);
}
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Database
public bool Equals(ILive<T>? other) => ID == other?.ID; public bool Equals(ILive<T>? other) => ID == other?.ID;
public override string ToString() => Value.ToString();
public Guid ID => Value.ID; public Guid ID => Value.ID;
public void PerformRead(Action<T> perform) => perform(Value); public void PerformRead(Action<T> perform) => perform(Value);

View File

@ -6,18 +6,13 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Database namespace osu.Game.Database
{ {
public class UserLookupCache : MemoryCachingComponent<int, APIUser> public class UserLookupCache : OnlineLookupCache<int, APIUser, GetUsersRequest>
{ {
[Resolved]
private IAPIProvider api { get; set; }
/// <summary> /// <summary>
/// Perform an API lookup on the specified user, populating a <see cref="APIUser"/> model. /// Perform an API lookup on the specified user, populating a <see cref="APIUser"/> model.
/// </summary> /// </summary>
@ -25,7 +20,7 @@ namespace osu.Game.Database
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated user, or null if the user does not exist or the request could not be satisfied.</returns> /// <returns>The populated user, or null if the user does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull] [ItemCanBeNull]
public Task<APIUser> GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); public Task<APIUser> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
/// <summary> /// <summary>
/// Perform an API lookup on the specified users, populating a <see cref="APIUser"/> model. /// Perform an API lookup on the specified users, populating a <see cref="APIUser"/> model.
@ -33,115 +28,10 @@ namespace osu.Game.Database
/// <param name="userIds">The users to lookup.</param> /// <param name="userIds">The users to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated users. May include null results for failed retrievals.</returns> /// <returns>The populated users. May include null results for failed retrievals.</returns>
public Task<APIUser[]> GetUsersAsync(int[] userIds, CancellationToken token = default) public Task<APIUser[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
{
var userLookupTasks = new List<Task<APIUser>>();
foreach (int u in userIds) protected override GetUsersRequest CreateRequest(IEnumerable<int> ids) => new GetUsersRequest(ids.ToArray());
{
userLookupTasks.Add(GetUserAsync(u, token).ContinueWith(task =>
{
if (!task.IsCompletedSuccessfully)
return null;
return task.Result; protected override IEnumerable<APIUser> RetrieveResults(GetUsersRequest request) => request.Response?.Users;
}, token));
}
return Task.WhenAll(userLookupTasks);
}
protected override async Task<APIUser> ComputeValueAsync(int lookup, CancellationToken token = default)
=> await queryUser(lookup).ConfigureAwait(false);
private readonly Queue<(int id, TaskCompletionSource<APIUser>)> pendingUserTasks = new Queue<(int, TaskCompletionSource<APIUser>)>();
private Task pendingRequestTask;
private readonly object taskAssignmentLock = new object();
private Task<APIUser> queryUser(int userId)
{
lock (taskAssignmentLock)
{
var tcs = new TaskCompletionSource<APIUser>();
// 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<int, List<TaskCompletionSource<APIUser>>>();
// Grab at most 50 unique user IDs from the queue.
lock (taskAssignmentLock)
{
while (pendingUserTasks.Count > 0 && userTasks.Count < 50)
{
(int id, TaskCompletionSource<APIUser> 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<TaskCompletionSource<APIUser>> { 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<APIUser> 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);
} }
} }

View File

@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using Humanizer.Localisation; using Humanizer.Localisation;
@ -10,6 +13,33 @@ namespace osu.Game.Online.Rooms
{ {
public static class PlaylistExtensions public static class PlaylistExtensions
{ {
/// <summary>
/// Returns all historical/expired items from the <paramref name="playlist"/>, in the order in which they were played.
/// </summary>
public static IEnumerable<PlaylistItem> GetHistoricalItems(this IEnumerable<PlaylistItem> playlist)
=> playlist.Where(item => item.Expired).OrderBy(item => item.PlayedAt);
/// <summary>
/// Returns all non-expired items from the <paramref name="playlist"/>, in the order in which they are to be played.
/// </summary>
public static IEnumerable<PlaylistItem> GetUpcomingItems(this IEnumerable<PlaylistItem> playlist)
=> playlist.Where(item => !item.Expired).OrderBy(item => item.PlaylistOrder);
/// <summary>
/// Returns the first non-expired <see cref="PlaylistItem"/> in playlist order from the supplied <paramref name="playlist"/>,
/// or the last-played <see cref="PlaylistItem"/> if all items are expired,
/// or <see langword="null"/> if <paramref name="playlist"/> was empty.
/// </summary>
public static PlaylistItem? GetCurrentItem(this ICollection<PlaylistItem> 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<PlaylistItem> playlist) => public static string GetTotalDuration(this BindableList<PlaylistItem> playlist) =>
playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2);
} }

View File

@ -680,7 +680,7 @@ namespace osu.Game
sessionIdleTracker.IsIdle.BindValueChanged(idle => sessionIdleTracker.IsIdle.BindValueChanged(idle =>
{ {
if (idle.NewValue) if (idle.NewValue)
SessionStatics.ResetValues(); SessionStatics.ResetAfterInactivity();
}); });
Add(sessionIdleTracker); Add(sessionIdleTracker);

View File

@ -33,7 +33,7 @@ namespace osu.Game.Overlays
private Drawable currentContent; private Drawable currentContent;
private Container panelTarget; private Container panelTarget;
private FillFlowContainer<BeatmapCard> foundContent; private FillFlowContainer<BeatmapCardNormal> foundContent;
private NotFoundDrawable notFoundContent; private NotFoundDrawable notFoundContent;
private SupporterRequiredDrawable supporterRequiredContent; private SupporterRequiredDrawable supporterRequiredContent;
private BeatmapListingFilterControl filterControl; private BeatmapListingFilterControl filterControl;
@ -78,7 +78,7 @@ namespace osu.Game.Overlays
Padding = new MarginPadding { Horizontal = 20 }, Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[] Children = new Drawable[]
{ {
foundContent = new FillFlowContainer<BeatmapCard>(), foundContent = new FillFlowContainer<BeatmapCardNormal>(),
notFoundContent = new NotFoundDrawable(), notFoundContent = new NotFoundDrawable(),
supporterRequiredContent = new SupporterRequiredDrawable(), supporterRequiredContent = new SupporterRequiredDrawable(),
} }
@ -135,7 +135,7 @@ namespace osu.Game.Overlays
return; return;
} }
var newPanels = searchResult.Results.Select(b => new BeatmapCard(b) var newPanels = searchResult.Results.Select(b => new BeatmapCardNormal(b)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = 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. // 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). // reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
var content = new ReverseChildIDFillFlowContainer<BeatmapCard> var content = new ReverseChildIDFillFlowContainer<BeatmapCardNormal>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -42,7 +42,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateStatistics(UserStatistics statistics) 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<int, int>(index, x)).Where(x => x.Value != 0).ToArray(); Data = userRanks?.Select((x, index) => new KeyValuePair<int, int>(index, x)).Where(x => x.Value != 0).ToArray();
} }

View File

@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
protected override Drawable CreateDrawableItem(APIBeatmapSet model) => model.OnlineID > 0 protected override Drawable CreateDrawableItem(APIBeatmapSet model) => model.OnlineID > 0
? new BeatmapCard(model) ? new BeatmapCardNormal(model)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,

View File

@ -136,12 +136,12 @@ namespace osu.Game.Overlays.Rankings
{ {
new ScoresTable(1, response.Users), 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). // reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
new ReverseChildIDFillFlowContainer<BeatmapCard> new ReverseChildIDFillFlowContainer<BeatmapCardNormal>
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Spacing = new Vector2(10), Spacing = new Vector2(10),
Children = response.BeatmapSets.Select(b => new BeatmapCard(b) Children = response.BeatmapSets.Select(b => new BeatmapCardNormal(b)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,

View File

@ -177,8 +177,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
else else
circle.Colour = colour; circle.Colour = colour;
var col = circle.Colour.TopLeft.Linear; var averageColour = Interpolation.ValueAt(0.5, circle.Colour.TopLeft, circle.Colour.TopRight, 0, 1);
colouredComponents.Colour = OsuColour.ForegroundTextColourFor(col); colouredComponents.Colour = OsuColour.ForegroundTextColourFor(averageColour);
} }
private SamplePointPiece sampleOverrideDisplay; private SamplePointPiece sampleOverrideDisplay;

View File

@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components namespace osu.Game.Screens.OnlinePlay.Components
{ {
@ -30,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
private void updateBeatmap() 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 }; protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both };

View File

@ -3,7 +3,6 @@
#nullable enable #nullable enable
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@ -20,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
public LoungeBackgroundScreen() public LoungeBackgroundScreen()
{ {
SelectedRoom.BindValueChanged(onSelectedRoomChanged); SelectedRoom.BindValueChanged(onSelectedRoomChanged);
playlist.BindCollectionChanged((_, __) => PlaylistItem = playlist.FirstOrDefault()); playlist.BindCollectionChanged((_, __) => PlaylistItem = playlist.GetCurrentItem());
} }
private void onSelectedRoomChanged(ValueChangedEvent<Room> room) private void onSelectedRoomChanged(ValueChangedEvent<Room> room)

View File

@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -434,6 +435,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void handleRoomLost() => Schedule(() => private void handleRoomLost() => Schedule(() =>
{ {
Logger.Log($"{this} exiting due to loss of room or connection");
if (this.IsCurrentScreen()) if (this.IsCurrentScreen())
this.Exit(); this.Exit();
else else

View File

@ -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) gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = stack = new OsuScreenStack() Child = stack = new OsuScreenStack
{
Name = nameof(PlayerArea),
}
}; };
stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock))); stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock)));

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -73,7 +72,7 @@ namespace osu.Game.Screens.OnlinePlay
private IBindable<PlaylistItem> subScreenSelectedItem { get; set; } private IBindable<PlaylistItem> subScreenSelectedItem { get; set; }
/// <summary> /// <summary>
/// The currently selected item in the <see cref="RoomSubScreen"/>, or the last item from <see cref="Playlist"/> /// The currently selected item in the <see cref="RoomSubScreen"/>, or the current item from <see cref="Playlist"/>
/// if this <see cref="OnlinePlayComposite"/> is not within a <see cref="RoomSubScreen"/>. /// if this <see cref="OnlinePlayComposite"/> is not within a <see cref="RoomSubScreen"/>.
/// </summary> /// </summary>
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>(); protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
@ -88,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay
protected virtual void UpdateSelectedItem() protected virtual void UpdateSelectedItem()
=> SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null => SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null
? Playlist.LastOrDefault() ? Playlist.GetCurrentItem()
: subScreenSelectedItem.Value; : subScreenSelectedItem.Value;
} }
} }

View File

@ -95,6 +95,8 @@ namespace osu.Game.Screens.OnlinePlay
private void forcefullyExit() 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 // This is temporary since we don't currently have a way to force screens to be exited
if (this.IsCurrentScreen()) if (this.IsCurrentScreen())
{ {

View File

@ -40,9 +40,16 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
if (gameplayClockContainer != null) if (gameplayClockContainer != null)
gameplayClockContainer.OnSeek += Clear; 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));
/// <summary>
/// Fired when a new judgement arrives.
/// </summary>
/// <param name="judgement">The new judgement.</param>
protected abstract void OnNewJudgement(JudgementResult judgement); protected abstract void OnNewJudgement(JudgementResult judgement);
protected Color4 GetColourForHitResult(HitResult result) protected Color4 GetColourForHitResult(HitResult result)
@ -84,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (processor != null) if (processor != null)
processor.NewJudgement -= OnNewJudgement; processor.NewJudgement -= processorNewJudgement;
if (gameplayClockContainer != null) if (gameplayClockContainer != null)
gameplayClockContainer.OnSeek -= Clear; gameplayClockContainer.OnSeek -= Clear;

View File

@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play
onlineBeatmapRequest.Success += beatmapSet => Schedule(() => onlineBeatmapRequest.Success += beatmapSet => Schedule(() =>
{ {
this.beatmapSet = beatmapSet; this.beatmapSet = beatmapSet;
beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet, allowExpansion: false); beatmapPanelContainer.Child = new BeatmapCardNormal(this.beatmapSet, allowExpansion: false);
checkForAutomaticDownload(); checkForAutomaticDownload();
}); });

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies; protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies;
public bool RoomJoined => RoomManager.RoomJoined; public bool RoomJoined => Client.RoomJoined;
private readonly bool joinRoom; private readonly bool joinRoom;

View File

@ -31,9 +31,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
private readonly Bindable<bool> isConnected = new Bindable<bool>(true); private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
public new Room? APIRoom => base.APIRoom; public new Room? APIRoom => base.APIRoom;
public Action<MultiplayerRoom>? RoomSetupAction; public Action<MultiplayerRoom>? RoomSetupAction;
public bool RoomJoined { get; private set; }
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; private IAPIProvider api { get; set; } = null!;
@ -49,7 +50,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex]; private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex];
private int currentIndex; private int currentIndex;
private long lastPlaylistItemId; private long lastPlaylistItemId;
public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) 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). // 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(); 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); public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);

View File

@ -17,8 +17,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// </summary> /// </summary>
public class TestMultiplayerRoomManager : MultiplayerRoomManager public class TestMultiplayerRoomManager : MultiplayerRoomManager
{ {
public bool RoomJoined { get; private set; }
private readonly TestRoomRequestsHandler requestsHandler; private readonly TestRoomRequestsHandler requestsHandler;
public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler) public TestMultiplayerRoomManager(TestRoomRequestsHandler requestsHandler)
@ -29,28 +27,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
public IReadOnlyList<Room> ServerSideRooms => requestsHandler.ServerSideRooms; public IReadOnlyList<Room> ServerSideRooms => requestsHandler.ServerSideRooms;
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{ => base.CreateRoom(room, r => onSuccess?.Invoke(r), onError);
base.CreateRoom(room, r =>
{
onSuccess?.Invoke(r);
RoomJoined = true;
}, onError);
}
public override void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null) public override void JoinRoom(Room room, string password = null, Action<Room> onSuccess = null, Action<string> onError = null)
{ => base.JoinRoom(room, password, r => onSuccess?.Invoke(r), onError);
base.JoinRoom(room, password, r =>
{
onSuccess?.Invoke(r);
RoomJoined = true;
}, onError);
}
public override void PartRoom()
{
base.PartRoom();
RoomJoined = false;
}
/// <summary> /// <summary>
/// Adds a room to a local "server-side" list that's returned when a <see cref="GetRoomsRequest"/> is fired. /// Adds a room to a local "server-side" list that's returned when a <see cref="GetRoomsRequest"/> is fired.

View File

@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Framework.Testing.Input; using osu.Framework.Testing.Input;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -11,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
{ {
@ -115,6 +119,25 @@ namespace osu.Game.Tests.Visual
}); });
} }
/// <summary>
/// Wait for a button to become enabled, then click it.
/// </summary>
/// <typeparam name="T"></typeparam>
protected void ClickButtonWhenEnabled<T>()
where T : Drawable
{
if (typeof(T) == typeof(Button))
AddUntilStep($"wait for {typeof(T).Name} enabled", () => (this.ChildrenOfType<T>().Single() as Button)?.Enabled.Value == true);
else
AddUntilStep($"wait for {typeof(T).Name} enabled", () => this.ChildrenOfType<T>().Single().ChildrenOfType<Button>().Single().Enabled.Value);
AddStep($"click {typeof(T).Name}", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<T>().Single());
InputManager.Click(MouseButton.Left);
});
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();

View File

@ -29,7 +29,11 @@ namespace osu.Game.Tests.Visual
{ {
base.Content.AddRange(new Drawable[] 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 }, content = new Container { RelativeSizeAxes = Axes.Both },
DialogOverlay = new DialogOverlay() DialogOverlay = new DialogOverlay()
}); });

View File

@ -27,6 +27,9 @@ namespace osu.Game.Users
public int Progress; public int Progress;
} }
[JsonProperty(@"is_ranked")]
public bool IsRanked;
[JsonProperty(@"global_rank")] [JsonProperty(@"global_rank")]
public int? GlobalRank; public int? GlobalRank;

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.7.1" /> <PackageReference Include="Realm" Version="10.7.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1217.1" /> <PackageReference Include="ppy.osu.Framework" Version="2021.1221.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" />
<PackageReference Include="Sentry" Version="3.12.1" /> <PackageReference Include="Sentry" Version="3.12.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />

View File

@ -60,7 +60,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1217.1" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1221.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -83,7 +83,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1217.1" /> <PackageReference Include="ppy.osu.Framework" Version="2021.1221.0" />
<PackageReference Include="SharpCompress" Version="0.30.0" /> <PackageReference Include="SharpCompress" Version="0.30.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />