1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 12:57:36 +08:00

Merge branch 'master' into fix-pofl-on-cooldown

This commit is contained in:
Salman Ahmed 2021-02-22 10:03:27 +03:00
commit 6bac83964a
57 changed files with 892 additions and 412 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.215.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.222.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -17,7 +17,7 @@ using osu.Game.Database;
namespace osu.Android namespace osu.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })] [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]

View File

@ -105,7 +105,7 @@ namespace osu.Desktop
if (privacyMode.Value == DiscordRichPresenceMode.Limited) if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
else else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty); presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
// update ruleset // update ruleset
presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom"; presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom";

View File

@ -4,6 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
@ -17,6 +18,10 @@ namespace osu.Game.Rulesets.Catch.Tests
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(5.0565038923984691d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new CatchRuleset(); protected override Ruleset CreateRuleset() => new CatchRuleset();

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
@ -17,6 +18,10 @@ namespace osu.Game.Rulesets.Mania.Tests
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(2.7646128945056723d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new ManiaRuleset(); protected override Ruleset CreateRuleset() => new ManiaRuleset();

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty; using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
@ -19,6 +20,11 @@ namespace osu.Game.Rulesets.Osu.Tests
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(8.6228371119393064d, "diffcalc-test")]
[TestCase(1.2864585434597433d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new OsuRuleset(); protected override Ruleset CreateRuleset() => new OsuRuleset();

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Taiko.Difficulty; using osu.Game.Rulesets.Taiko.Difficulty;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests
@ -18,6 +19,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(3.1473940254109078d, "diffcalc-test")]
[TestCase(3.1473940254109078d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset(), beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset(), beatmap);
protected override Ruleset CreateRuleset() => new TaikoRuleset(); protected override Ruleset CreateRuleset() => new TaikoRuleset();

View File

@ -118,9 +118,13 @@ namespace osu.Game.Tests.Rulesets
public BindableNumber<double> Frequency => throw new NotImplementedException(); public BindableNumber<double> Frequency => throw new NotImplementedException();
public BindableNumber<double> Tempo => throw new NotImplementedException(); public BindableNumber<double> Tempo => throw new NotImplementedException();
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException(); public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException(); public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException(); public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException();

View File

@ -56,9 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay
pauseAndConfirm(); pauseAndConfirm();
resume(); resume();
confirmClockRunning(false); confirmPausedWithNoOverlay();
confirmPauseOverlayShown(false);
AddStep("click to resume", () => InputManager.Click(MouseButton.Left)); AddStep("click to resume", () => InputManager.Click(MouseButton.Left));
confirmClockRunning(true); confirmClockRunning(true);
@ -71,15 +69,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1); AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1);
pauseAndConfirm(); pauseAndConfirm();
resume(); resume();
confirmClockRunning(false);
confirmPauseOverlayShown(false);
confirmPausedWithNoOverlay();
pauseAndConfirm(); pauseAndConfirm();
AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden); AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden);
confirmPaused(); confirmPaused();
confirmNotExited();
} }
[Test] [Test]
@ -94,33 +91,54 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
[Test] [Test]
public void TestPauseTooSoon() public void TestUserPauseWhenPauseNotAllowed()
{
AddStep("disable pause support", () => Player.Configuration.AllowPause = false);
pauseFromUserExitKey();
confirmExited();
}
[Test]
public void TestUserPauseDuringCooldownTooSoon()
{ {
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm(); pauseAndConfirm();
resume(); resume();
pause(); pauseFromUserExitKey();
confirmClockRunning(true); confirmResumed();
confirmPauseOverlayShown(false); confirmNotExited();
} }
[Test] [Test]
public void TestExitTooSoon() public void TestQuickExitDuringCooldownTooSoon()
{
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm();
resume();
AddStep("pause via exit key", () => Player.ExitViaQuickExit());
confirmResumed();
AddAssert("exited", () => !Player.IsCurrentScreen());
}
[Test]
public void TestExitSoonAfterResumeSucceeds()
{ {
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000)); AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
pauseAndConfirm(); pauseAndConfirm();
resume(); resume();
AddStep("exit too soon", () => Player.Exit()); AddStep("exit quick", () => Player.Exit());
confirmClockRunning(true); confirmResumed();
confirmPauseOverlayShown(false); AddAssert("exited", () => !Player.IsCurrentScreen());
AddAssert("not exited", () => Player.IsCurrentScreen());
} }
[Test] [Test]
@ -131,22 +149,37 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmClockRunning(false); confirmClockRunning(false);
pause(); AddStep("pause via forced pause", () => Player.Pause());
confirmClockRunning(false);
confirmPauseOverlayShown(false);
confirmPausedWithNoOverlay();
AddAssert("fail overlay still shown", () => Player.FailOverlayVisible); AddAssert("fail overlay still shown", () => Player.FailOverlayVisible);
exitAndConfirm(); exitAndConfirm();
} }
[Test] [Test]
public void TestExitFromFailedGameplay() public void TestExitFromFailedGameplayAfterFailAnimation()
{ {
AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("exit", () => Player.Exit()); AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible);
confirmClockRunning(false);
AddStep("exit via user pause", () => Player.ExitViaPause());
confirmExited();
}
[Test]
public void TestExitFromFailedGameplayDuringFailAnimation()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
// will finish the fail animation and show the fail/pause screen.
AddStep("attempt exit via pause key", () => Player.ExitViaPause());
AddAssert("fail overlay shown", () => Player.FailOverlayVisible);
// will actually exit.
AddStep("exit via pause key", () => Player.ExitViaPause());
confirmExited(); confirmExited();
} }
@ -245,7 +278,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void pauseAndConfirm() private void pauseAndConfirm()
{ {
pause(); pauseFromUserExitKey();
confirmPaused(); confirmPaused();
} }
@ -257,7 +290,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void exitAndConfirm() private void exitAndConfirm()
{ {
AddUntilStep("player not exited", () => Player.IsCurrentScreen()); confirmNotExited();
AddStep("exit", () => Player.Exit()); AddStep("exit", () => Player.Exit());
confirmExited(); confirmExited();
confirmNoTrackAdjustments(); confirmNoTrackAdjustments();
@ -266,7 +299,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void confirmPaused() private void confirmPaused()
{ {
confirmClockRunning(false); confirmClockRunning(false);
AddAssert("player not exited", () => Player.IsCurrentScreen()); confirmNotExited();
AddAssert("player not failed", () => !Player.HasFailed); AddAssert("player not failed", () => !Player.HasFailed);
AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); AddAssert("pause overlay shown", () => Player.PauseOverlayVisible);
} }
@ -277,18 +310,22 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmPauseOverlayShown(false); confirmPauseOverlayShown(false);
} }
private void confirmExited() private void confirmPausedWithNoOverlay()
{ {
AddUntilStep("player exited", () => !Player.IsCurrentScreen()); confirmClockRunning(false);
confirmPauseOverlayShown(false);
} }
private void confirmExited() => AddUntilStep("player exited", () => !Player.IsCurrentScreen());
private void confirmNotExited() => AddAssert("player not exited", () => Player.IsCurrentScreen());
private void confirmNoTrackAdjustments() private void confirmNoTrackAdjustments()
{ {
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1); AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1);
} }
private void restart() => AddStep("restart", () => Player.Restart()); private void restart() => AddStep("restart", () => Player.Restart());
private void pause() => AddStep("pause", () => Player.Pause()); private void pauseFromUserExitKey() => AddStep("user pause", () => Player.ExitViaPause());
private void resume() => AddStep("resume", () => Player.Resume()); private void resume() => AddStep("resume", () => Player.Resume());
private void confirmPauseOverlayShown(bool isShown) => private void confirmPauseOverlayShown(bool isShown) =>
@ -307,6 +344,10 @@ namespace osu.Game.Tests.Visual.Gameplay
public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible; public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible;
public void ExitViaPause() => PerformExit(true);
public void ExitViaQuickExit() => PerformExit(false);
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);

View File

@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
@ -50,6 +51,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = lookupCache.GetUserAsync(1).Result);
AddStep("create leaderboard", () => AddStep("create leaderboard", () =>
{ {
leaderboard?.Expire(); leaderboard?.Expire();
@ -85,6 +88,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestScoreUpdates() public void TestScoreUpdates()
{ {
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
} }
[Test] [Test]

View File

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -155,7 +156,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Id = i, Id = i,
Username = $"User {i}", Username = $"User {i}",
CurrentModeRank = RNG.Next(1, 100000), RulesetsStatistics = new Dictionary<string, UserStatistics>
{
{
Ruleset.Value.ShortName,
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
}
},
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}); });
@ -193,7 +200,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Id = 0, Id = 0,
Username = "User 0", Username = "User 0",
CurrentModeRank = RNG.Next(1, 100000), RulesetsStatistics = new Dictionary<string, UserStatistics>
{
{
Ruleset.Value.ShortName,
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
}
},
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}); });

View File

@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
@ -115,6 +116,8 @@ namespace osu.Game.Tests.Visual.Navigation
public new Bindable<RulesetInfo> Ruleset => base.Ruleset; public new Bindable<RulesetInfo> Ruleset => base.Ruleset;
public new Bindable<IReadOnlyList<Mod>> SelectedMods => base.SelectedMods;
// if we don't do this, when running under nUnit the version that gets populated is that of nUnit. // if we don't do this, when running under nUnit the version that gets populated is that of nUnit.
public override string Version => "test game"; public override string Version => "test game";

View File

@ -11,7 +11,9 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
@ -41,6 +43,30 @@ namespace osu.Game.Tests.Visual.Navigation
exitViaEscapeAndConfirm(); exitViaEscapeAndConfirm();
} }
[Test]
public void TestRetryFromResults()
{
Player player = null;
ResultsScreen results = null;
WorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new TestSongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("set autoplay", () => Game.SelectedMods.Value = new[] { new OsuModAutoplay() });
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
AddStep("seek to end", () => beatmap().Track.Seek(beatmap().Track.Length));
AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded);
AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player);
}
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public void TestSongContinuesAfterExitPlayer(bool withUserPause) public void TestSongContinuesAfterExitPlayer(bool withUserPause)

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
graph.Statistics.Value = new UserStatistics graph.Statistics.Value = new UserStatistics
{ {
Ranks = new UserStatistics.UserRanks { Global = 123456 }, GlobalRank = 123456,
PP = 12345, PP = 12345,
}; };
}); });
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
graph.Statistics.Value = new UserStatistics graph.Statistics.Value = new UserStatistics
{ {
Ranks = new UserStatistics.UserRanks { Global = 89000 }, GlobalRank = 89000,
PP = 12345, PP = 12345,
RankHistory = new User.RankHistoryData RankHistory = new User.RankHistoryData
{ {
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
graph.Statistics.Value = new UserStatistics graph.Statistics.Value = new UserStatistics
{ {
Ranks = new UserStatistics.UserRanks { Global = 89000 }, GlobalRank = 89000,
PP = 12345, PP = 12345,
RankHistory = new User.RankHistoryData RankHistory = new User.RankHistoryData
{ {
@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
graph.Statistics.Value = new UserStatistics graph.Statistics.Value = new UserStatistics
{ {
Ranks = new UserStatistics.UserRanks { Global = 12000 }, GlobalRank = 12000,
PP = 12345, PP = 12345,
RankHistory = new User.RankHistoryData RankHistory = new User.RankHistoryData
{ {
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
graph.Statistics.Value = new UserStatistics graph.Statistics.Value = new UserStatistics
{ {
Ranks = new UserStatistics.UserRanks { Global = 12000 }, GlobalRank = 12000,
PP = 12345, PP = 12345,
RankHistory = new User.RankHistoryData RankHistory = new User.RankHistoryData
{ {

View File

@ -33,7 +33,8 @@ namespace osu.Game.Tests.Visual.Online
ProfileOrder = new[] { "me" }, ProfileOrder = new[] { "me" },
Statistics = new UserStatistics Statistics = new UserStatistics
{ {
Ranks = new UserStatistics.UserRanks { Global = 2148, Country = 1 }, GlobalRank = 2148,
CountryRank = 1,
PP = 4567.89m, PP = 4567.89m,
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
{ {

View File

@ -113,11 +113,11 @@ namespace osu.Game.Tournament.Tests
}, },
Players = Players =
{ {
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 12 } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 16 } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 20 } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 24 } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 30 } },
} }
} }
}, },

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Models
{ {
get get
{ {
var ranks = Players.Select(p => p.Statistics?.Ranks.Global) var ranks = Players.Select(p => p.Statistics?.GlobalRank)
.Where(i => i.HasValue) .Where(i => i.HasValue)
.Select(i => i.Value) .Select(i => i.Value)
.ToArray(); .ToArray();

View File

@ -250,7 +250,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
}; };
foreach (var p in team.Players) foreach (var p in team.Players)
fill.Add(new RowDisplay(p.Username, p.Statistics?.Ranks.Global?.ToString("\\##,0") ?? "-")); fill.Add(new RowDisplay(p.Username, p.Statistics?.GlobalRank?.ToString("\\##,0") ?? "-"));
} }
internal class RowDisplay : CompositeDrawable internal class RowDisplay : CompositeDrawable

View File

@ -150,9 +150,9 @@ namespace osu.Game.Tournament
{ {
foreach (var p in t.Players) foreach (var p in t.Players)
{ {
if (string.IsNullOrEmpty(p.Username) || p.Statistics == null) if (string.IsNullOrEmpty(p.Username) || p.Statistics?.GlobalRank == null)
{ {
PopulateUser(p); PopulateUser(p, immediate: true);
addedInfo = true; addedInfo = true;
} }
} }
@ -211,12 +211,14 @@ namespace osu.Game.Tournament
return addedInfo; return addedInfo;
} }
public void PopulateUser(User user, Action success = null, Action failure = null) public void PopulateUser(User user, Action success = null, Action failure = null, bool immediate = false)
{ {
var req = new GetUserRequest(user.Id, Ruleset.Value); var req = new GetUserRequest(user.Id, Ruleset.Value);
req.Success += res => req.Success += res =>
{ {
user.Id = res.Id;
user.Username = res.Username; user.Username = res.Username;
user.Statistics = res.Statistics; user.Statistics = res.Statistics;
user.Country = res.Country; user.Country = res.Country;
@ -231,7 +233,10 @@ namespace osu.Game.Tournament
failure?.Invoke(); failure?.Invoke();
}; };
API.Queue(req); if (immediate)
API.Perform(req);
else
API.Queue(req);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -29,6 +29,14 @@ namespace osu.Game.Collections
/// </summary> /// </summary>
protected virtual bool ShowManageCollectionsItem => true; protected virtual bool ShowManageCollectionsItem => true;
private readonly BindableWithCurrent<CollectionFilterMenuItem> current = new BindableWithCurrent<CollectionFilterMenuItem>();
public new Bindable<CollectionFilterMenuItem> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly IBindableList<BeatmapCollection> collections = new BindableList<BeatmapCollection>(); private readonly IBindableList<BeatmapCollection> collections = new BindableList<BeatmapCollection>();
private readonly IBindableList<BeatmapInfo> beatmaps = new BindableList<BeatmapInfo>(); private readonly IBindableList<BeatmapInfo> beatmaps = new BindableList<BeatmapInfo>();
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>(); private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
@ -36,25 +44,28 @@ namespace osu.Game.Collections
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; } private ManageCollectionsDialog manageCollectionsDialog { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
public CollectionFilterDropdown() public CollectionFilterDropdown()
{ {
ItemSource = filters; ItemSource = filters;
} Current.Value = new AllBeatmapsCollectionFilterMenuItem();
[BackgroundDependencyLoader(permitNulls: true)]
private void load([CanBeNull] CollectionManager collectionManager)
{
if (collectionManager != null)
collections.BindTo(collectionManager.Collections);
collections.CollectionChanged += (_, __) => collectionsChanged();
collectionsChanged();
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
if (collectionManager != null)
collections.BindTo(collectionManager.Collections);
// Dropdown has logic which triggers a change on the bindable with every change to the contained items.
// This is not desirable here, as it leads to multiple filter operations running even though nothing has changed.
// An extra bindable is enough to subvert this behaviour.
base.Current = Current;
collections.BindCollectionChanged((_, __) => collectionsChanged(), true);
Current.BindValueChanged(filterChanged, true); Current.BindValueChanged(filterChanged, true);
} }

View File

@ -1,6 +1,7 @@
// 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 JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -9,7 +10,7 @@ namespace osu.Game.Collections
/// <summary> /// <summary>
/// A <see cref="BeatmapCollection"/> filter. /// A <see cref="BeatmapCollection"/> filter.
/// </summary> /// </summary>
public class CollectionFilterMenuItem public class CollectionFilterMenuItem : IEquatable<CollectionFilterMenuItem>
{ {
/// <summary> /// <summary>
/// The collection to filter beatmaps from. /// The collection to filter beatmaps from.
@ -33,6 +34,11 @@ namespace osu.Game.Collections
Collection = collection; Collection = collection;
CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable<string>("All beatmaps"); CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable<string>("All beatmaps");
} }
public bool Equals(CollectionFilterMenuItem other)
=> other != null && CollectionName.Value == other.CollectionName.Value;
public override int GetHashCode() => CollectionName.Value.GetHashCode();
} }
public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem

View File

@ -16,12 +16,12 @@ namespace osu.Game.Configuration
[Description("Hit Error (right)")] [Description("Hit Error (right)")]
HitErrorRight, HitErrorRight,
[Description("Hit Error (bottom)")]
HitErrorBottom,
[Description("Hit Error (left+right)")] [Description("Hit Error (left+right)")]
HitErrorBoth, HitErrorBoth,
[Description("Hit Error (bottom)")]
HitErrorBottom,
[Description("Colour (left)")] [Description("Colour (left)")]
ColourLeft, ColourLeft,

View File

@ -36,18 +36,23 @@ namespace osu.Game.Online.Multiplayer
[Key(5)] [Key(5)]
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>(); public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();
[Key(6)]
public long PlaylistItemId { get; set; }
public bool Equals(MultiplayerRoomSettings other) public bool Equals(MultiplayerRoomSettings other)
=> BeatmapID == other.BeatmapID => BeatmapID == other.BeatmapID
&& BeatmapChecksum == other.BeatmapChecksum && BeatmapChecksum == other.BeatmapChecksum
&& RequiredMods.SequenceEqual(other.RequiredMods) && RequiredMods.SequenceEqual(other.RequiredMods)
&& AllowedMods.SequenceEqual(other.AllowedMods) && AllowedMods.SequenceEqual(other.AllowedMods)
&& RulesetID == other.RulesetID && RulesetID == other.RulesetID
&& Name.Equals(other.Name, StringComparison.Ordinal); && Name.Equals(other.Name, StringComparison.Ordinal)
&& PlaylistItemId == other.PlaylistItemId;
public override string ToString() => $"Name:{Name}" public override string ToString() => $"Name:{Name}"
+ $" Beatmap:{BeatmapID} ({BeatmapChecksum})" + $" Beatmap:{BeatmapID} ({BeatmapChecksum})"
+ $" RequiredMods:{string.Join(',', RequiredMods)}" + $" RequiredMods:{string.Join(',', RequiredMods)}"
+ $" AllowedMods:{string.Join(',', AllowedMods)}" + $" AllowedMods:{string.Join(',', AllowedMods)}"
+ $" Ruleset:{RulesetID}"; + $" Ruleset:{RulesetID}"
+ $" Item:{PlaylistItemId}";
} }
} }

View File

@ -66,6 +66,8 @@ namespace osu.Game.Online.Multiplayer
/// </summary> /// </summary>
public readonly BindableList<int> CurrentMatchPlayingUserIds = new BindableList<int>(); public readonly BindableList<int> CurrentMatchPlayingUserIds = new BindableList<int>();
public readonly Bindable<PlaylistItem?> CurrentMatchPlayingItem = new Bindable<PlaylistItem?>();
/// <summary> /// <summary>
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available. /// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
/// </summary> /// </summary>
@ -92,10 +94,11 @@ namespace osu.Game.Online.Multiplayer
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } = null!; private RulesetStore rulesets { get; set; } = null!;
private Room? apiRoom; // Only exists for compatibility with old osu-server-spectator build.
// Todo: Can be removed on 2021/02/26.
private long defaultPlaylistItemId;
// Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. private Room? apiRoom;
private long playlistItemId;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -142,7 +145,7 @@ namespace osu.Game.Online.Multiplayer
{ {
Room = joinedRoom; Room = joinedRoom;
apiRoom = room; apiRoom = room;
playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0;
}, cancellationSource.Token); }, cancellationSource.Token);
// Update room settings. // Update room settings.
@ -218,7 +221,7 @@ namespace osu.Game.Online.Multiplayer
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
RulesetID = item.GetOr(existingPlaylistItem).RulesetID, RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods, RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods,
AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods,
}); });
} }
@ -506,14 +509,13 @@ namespace osu.Game.Online.Multiplayer
Room.Settings = settings; Room.Settings = settings;
apiRoom.Name.Value = Room.Settings.Name; apiRoom.Name.Value = Room.Settings.Name;
// The playlist update is delayed until an online beatmap lookup (below) succeeds. // The current item update is delayed until an online beatmap lookup (below) succeeds.
// In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. // In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here.
apiRoom.Playlist.Clear(); CurrentMatchPlayingItem.Value = null;
RoomUpdated?.Invoke(); RoomUpdated?.Invoke();
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
req.Success += res => req.Success += res =>
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
@ -540,18 +542,30 @@ namespace osu.Game.Online.Multiplayer
var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset)); var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
PlaylistItem playlistItem = new PlaylistItem // Try to retrieve the existing playlist item from the API room.
var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId);
if (playlistItem != null)
updateItem(playlistItem);
else
{ {
ID = playlistItemId, // An existing playlist item does not exist, so append a new one.
Beatmap = { Value = beatmap }, updateItem(playlistItem = new PlaylistItem());
Ruleset = { Value = ruleset.RulesetInfo }, apiRoom.Playlist.Add(playlistItem);
}; }
playlistItem.RequiredMods.AddRange(mods); CurrentMatchPlayingItem.Value = playlistItem;
playlistItem.AllowedMods.AddRange(allowedMods);
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. void updateItem(PlaylistItem item)
apiRoom.Playlist.Add(playlistItem); {
item.ID = settings.PlaylistItemId == 0 ? defaultPlaylistItemId : settings.PlaylistItemId;
item.Beatmap.Value = beatmap;
item.Ruleset.Value = ruleset.RulesetInfo;
item.RequiredMods.Clear();
item.RequiredMods.AddRange(mods);
item.AllowedMods.Clear();
item.AllowedMods.AddRange(allowedMods);
}
} }
/// <summary> /// <summary>

View File

@ -23,6 +23,12 @@ namespace osu.Game.Online.Rooms
[JsonProperty("ruleset_id")] [JsonProperty("ruleset_id")]
public int RulesetID { get; set; } public int RulesetID { get; set; }
/// <summary>
/// Whether this <see cref="PlaylistItem"/> is still a valid selection for the <see cref="Room"/>.
/// </summary>
[JsonProperty("expired")]
public bool Expired { get; set; }
[JsonIgnore] [JsonIgnore]
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>(); public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();

View File

@ -153,6 +153,12 @@ namespace osu.Game.Online.Rooms
if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value)
Status.Value = new RoomStatusEnded(); Status.Value = new RoomStatusEnded();
// Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended,
// and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room.
// More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room.
if (!(Status.Value is RoomStatusEnded))
other.Playlist.RemoveAll(i => i.Expired);
if (!Playlist.SequenceEqual(other.Playlist)) if (!Playlist.SequenceEqual(other.Playlist))
{ {
Playlist.Clear(); Playlist.Clear();

View File

@ -383,7 +383,7 @@ namespace osu.Game
Ruleset.Value = selection.Ruleset; Ruleset.Value = selection.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
}, validScreens: new[] { typeof(PlaySongSelect) }); }, validScreens: new[] { typeof(SongSelect) });
} }
/// <summary> /// <summary>

View File

@ -30,7 +30,7 @@ namespace osu.Game.Overlays
protected List<APIUpdateStream> Streams; protected List<APIUpdateStream> Streams;
public ChangelogOverlay() public ChangelogOverlay()
: base(OverlayColourScheme.Purple) : base(OverlayColourScheme.Purple, false)
{ {
} }

View File

@ -14,7 +14,7 @@ namespace osu.Game.Overlays
private readonly Bindable<string> article = new Bindable<string>(null); private readonly Bindable<string> article = new Bindable<string>(null);
public NewsOverlay() public NewsOverlay()
: base(OverlayColourScheme.Purple) : base(OverlayColourScheme.Purple, false)
{ {
} }

View File

@ -4,6 +4,7 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -16,10 +17,16 @@ namespace osu.Game.Overlays
protected readonly LoadingLayer Loading; protected readonly LoadingLayer Loading;
private readonly Container content; private readonly Container content;
protected OnlineOverlay(OverlayColourScheme colourScheme) protected OnlineOverlay(OverlayColourScheme colourScheme, bool requiresSignIn = true)
: base(colourScheme) : base(colourScheme)
{ {
base.Content.AddRange(new Drawable[] var mainContent = requiresSignIn
? new OnlineViewContainer($"Sign in to view the {Header.Title.Title}")
: new Container();
mainContent.RelativeSizeAxes = Axes.Both;
mainContent.AddRange(new Drawable[]
{ {
ScrollFlow = new OverlayScrollContainer ScrollFlow = new OverlayScrollContainer
{ {
@ -43,6 +50,8 @@ namespace osu.Game.Overlays
}, },
Loading = new LoadingLayer(true) Loading = new LoadingLayer(true)
}); });
base.Content.Add(mainContent);
} }
} }
} }

View File

@ -144,8 +144,8 @@ namespace osu.Game.Overlays.Profile.Header
private void updateDisplay(User user) private void updateDisplay(User user)
{ {
hiddenDetailGlobal.Content = user?.Statistics?.Ranks.Global?.ToString("\\##,##0") ?? "-"; hiddenDetailGlobal.Content = user?.Statistics?.GlobalRank?.ToString("\\##,##0") ?? "-";
hiddenDetailCountry.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-"; hiddenDetailCountry.Content = user?.Statistics?.CountryRank?.ToString("\\##,##0") ?? "-";
} }
} }
} }

View File

@ -176,8 +176,8 @@ namespace osu.Game.Overlays.Profile.Header
foreach (var scoreRankInfo in scoreRankInfos) foreach (var scoreRankInfo in scoreRankInfos)
scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0;
detailGlobalRank.Content = user?.Statistics?.Ranks.Global?.ToString("\\##,##0") ?? "-"; detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToString("\\##,##0") ?? "-";
detailCountryRank.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-"; detailCountryRank.Content = user?.Statistics?.CountryRank?.ToString("\\##,##0") ?? "-";
rankGraph.Statistics.Value = user?.Statistics; rankGraph.Statistics.Value = user?.Statistics;
} }

View File

@ -61,8 +61,7 @@ namespace osu.Game.Overlays
LoadComponentAsync(display, loaded => LoadComponentAsync(display, loaded =>
{ {
if (API.IsLoggedIn) Loading.Hide();
Loading.Hide();
Child = loaded; Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token); }, (cancellationToken = new CancellationTokenSource()).Token);

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
namespace osu.Game namespace osu.Game
@ -81,24 +82,45 @@ namespace osu.Game
game?.CloseAllOverlays(false); game?.CloseAllOverlays(false);
// we may already be at the target screen type. findValidTarget(current);
if (validScreens.Contains(current.GetType()) && !beatmap.Disabled) }
private bool findValidTarget(IScreen current)
{
var type = current.GetType();
// check if we are already at a valid target screen.
if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled)
{ {
finalAction(current); finalAction(current);
Cancel(); Cancel();
return; return true;
} }
while (current != null) while (current != null)
{ {
if (validScreens.Contains(current.GetType())) // if this has a sub stack, recursively check the screens within it.
if (current is IHasSubScreenStack currentSubScreen)
{
if (findValidTarget(currentSubScreen.SubScreenStack.CurrentScreen))
{
// should be correct in theory, but currently untested/unused in existing implementations.
current.MakeCurrent();
return true;
}
}
if (validScreens.Any(t => t.IsAssignableFrom(type)))
{ {
current.MakeCurrent(); current.MakeCurrent();
break; return true;
} }
current = current.GetParentScreen(); current = current.GetParentScreen();
type = current?.GetType();
} }
return false;
} }
/// <summary> /// <summary>

View File

@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.UI
public IEnumerable<string> GetAvailableResources() => throw new NotSupportedException(); public IEnumerable<string> GetAvailableResources() => throw new NotSupportedException();
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException(); public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException(); public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException(); public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();
@ -134,6 +134,10 @@ namespace osu.Game.Rulesets.UI
public IBindable<double> AggregateTempo => throw new NotSupportedException(); public IBindable<double> AggregateTempo => throw new NotSupportedException();
public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
public int PlaybackConcurrency public int PlaybackConcurrency
{ {
get => throw new NotSupportedException(); get => throw new NotSupportedException();

View File

@ -0,0 +1,15 @@
// 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 osu.Framework.Screens;
namespace osu.Game.Screens
{
/// <summary>
/// A screen which manages a nested stack of screens within itself.
/// </summary>
public interface IHasSubScreenStack
{
ScreenStack SubScreenStack { get; }
}
}

View File

@ -8,11 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -21,14 +24,20 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Cached(typeof(IPreviewTrackOwner))] [Cached(typeof(IPreviewTrackOwner))]
public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner
{ {
[Cached(typeof(IBindable<PlaylistItem>))]
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>(); protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
private Sample sampleStart; private readonly ModSelectOverlay userModsSelectOverlay;
[Resolved(typeof(Room), nameof(Room.Playlist))] /// <summary>
protected BindableList<PlaylistItem> Playlist { get; private set; } /// A container that provides controls for selection of user mods.
/// This will be shown/hidden automatically when applicable.
/// </summary>
protected Drawable UserModsSection;
private Sample sampleStart;
/// <summary> /// <summary>
/// Any mods applied by/to the local user. /// Any mods applied by/to the local user.
@ -53,9 +62,26 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected RoomSubScreen() protected RoomSubScreen()
{ {
AddInternal(BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker AddRangeInternal(new Drawable[]
{ {
SelectedItem = { BindTarget = SelectedItem } BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker
{
SelectedItem = { BindTarget = SelectedItem }
},
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Depth = float.MinValue,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
IsValidMod = _ => false
}
},
}); });
} }
@ -73,7 +99,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
base.LoadComplete(); base.LoadComplete();
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
SelectedItem.Value = Playlist.FirstOrDefault();
managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated); managerUpdated.BindValueChanged(beatmapUpdated);
@ -81,6 +106,19 @@ namespace osu.Game.Screens.OnlinePlay.Match
UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods));
} }
public override bool OnBackButton()
{
if (userModsSelectOverlay.State.Value == Visibility.Visible)
{
userModsSelectOverlay.Hide();
return true;
}
return base.OnBackButton();
}
protected void ShowUserModSelect() => userModsSelectOverlay.Show();
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
@ -120,17 +158,31 @@ namespace osu.Game.Screens.OnlinePlay.Match
{ {
updateWorkingBeatmap(); updateWorkingBeatmap();
if (SelectedItem.Value == null) var selected = SelectedItem.Value;
if (selected == null)
return; return;
// Remove any user mods that are no longer allowed. // Remove any user mods that are no longer allowed.
UserMods.Value = UserMods.Value UserMods.Value = UserMods.Value
.Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType())) .Where(m => selected.AllowedMods.Any(a => m.GetType() == a.GetType()))
.ToList(); .ToList();
UpdateMods(); UpdateMods();
Ruleset.Value = SelectedItem.Value.Ruleset.Value; Ruleset.Value = selected.Ruleset.Value;
if (!selected.AllowedMods.Any())
{
UserModsSection?.Hide();
userModsSelectOverlay.Hide();
userModsSelectOverlay.IsValidMod = _ => false;
}
else
{
UserModsSection?.Show();
userModsSelectOverlay.IsValidMod = m => selected.AllowedMods.Any(a => a.GetType() == m.GetType());
}
} }
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet) => Schedule(updateWorkingBeatmap); private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet) => Schedule(updateWorkingBeatmap);
@ -185,5 +237,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
if (track != null) if (track != null)
track.Looping = false; track.Looping = false;
} }
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
{
}
} }
} }

View File

@ -1,8 +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 System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -13,7 +11,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
public class BeatmapSelectionControl : OnlinePlayComposite public class BeatmapSelectionControl : RoomSubScreenComposite
{ {
[Resolved] [Resolved]
private MultiplayerMatchSubScreen matchSubScreen { get; set; } private MultiplayerMatchSubScreen matchSubScreen { get; set; }
@ -60,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
base.LoadComplete(); base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true); SelectedItem.BindValueChanged(_ => updateBeatmap(), true);
Host.BindValueChanged(host => Host.BindValueChanged(host =>
{ {
if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true) if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true)
@ -70,12 +68,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}, true); }, true);
} }
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) private void updateBeatmap()
{ {
if (Playlist.Any()) if (SelectedItem.Value == null)
beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(Playlist.Single(), false, false);
else
beatmapPanelContainer.Clear(); beatmapPanelContainer.Clear();
else
beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(SelectedItem.Value, false, false);
} }
} }
} }

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -16,7 +15,6 @@ using osu.Framework.Threading;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
@ -43,11 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved] [Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; } private OngoingOperationTracker ongoingOperationTracker { get; set; }
private ModSelectOverlay userModsSelectOverlay;
private MultiplayerMatchSettingsOverlay settingsOverlay; private MultiplayerMatchSettingsOverlay settingsOverlay;
private Drawable userModsSection;
private IBindable<bool> isConnected; private readonly IBindable<bool> isConnected = new Bindable<bool>();
[CanBeNull] [CanBeNull]
private IDisposable readyClickOperation; private IDisposable readyClickOperation;
@ -155,7 +151,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } new BeatmapSelectionControl { RelativeSizeAxes = Axes.X }
} }
}, },
userModsSection = new FillFlowContainer UserModsSection = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -176,7 +172,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Width = 90, Width = 90,
Text = "Select", Text = "Select",
Action = () => userModsSelectOverlay.Show() Action = ShowUserModSelect,
}, },
new ModDisplay new ModDisplay
{ {
@ -231,19 +227,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
} }
}, },
new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
IsValidMod = _ => false
}
},
settingsOverlay = new MultiplayerMatchSettingsOverlay settingsOverlay = new MultiplayerMatchSettingsOverlay
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -269,14 +252,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
base.LoadComplete(); base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true); SelectedItem.BindTo(client.CurrentMatchPlayingItem);
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged); UserMods.BindValueChanged(onUserModsChanged);
client.LoadRequested += onLoadRequested; client.LoadRequested += onLoadRequested;
client.RoomUpdated += onRoomUpdated; client.RoomUpdated += onRoomUpdated;
isConnected = client.IsConnected.GetBoundCopy(); isConnected.BindTo(client.IsConnected);
isConnected.BindValueChanged(connected => isConnected.BindValueChanged(connected =>
{ {
if (!connected.NewValue) if (!connected.NewValue)
@ -303,32 +287,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return true; return true;
} }
if (userModsSelectOverlay.State.Value == Visibility.Visible)
{
userModsSelectOverlay.Hide();
return true;
}
return base.OnBackButton(); return base.OnBackButton();
} }
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e)
{
SelectedItem.Value = Playlist.FirstOrDefault();
if (SelectedItem.Value?.AllowedMods.Any() != true)
{
userModsSection.Hide();
userModsSelectOverlay.Hide();
userModsSelectOverlay.IsValidMod = _ => false;
}
else
{
userModsSection.Show();
userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType());
}
}
private ModSettingChangeTracker modSettingChangeTracker; private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedModSettingsUpdate; private ScheduledDelegate debouncedModSettingsUpdate;
@ -433,9 +394,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
modSettingChangeTracker?.Dispose(); modSettingChangeTracker?.Dispose();
} }
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
{
}
} }
} }

View File

@ -89,6 +89,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(client.Room != null); Debug.Assert(client.Room != null);
} }
protected override void LoadComplete()
{
base.LoadComplete();
((IBindable<bool>)leaderboard.Expanded).BindTo(IsBreakTime);
}
protected override void StartGameplay() protected override void StartGameplay()
{ {
// block base call, but let the server know we are ready to start. // block base call, but let the server know we are ready to start.

View File

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -35,9 +36,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
private SpriteIcon crown;
private OsuSpriteText userRankText;
private ModDisplay userModsDisplay; private ModDisplay userModsDisplay;
private StateDisplay userStateDisplay; private StateDisplay userStateDisplay;
private SpriteIcon crown;
public ParticipantPanel(MultiplayerRoomUser user) public ParticipantPanel(MultiplayerRoomUser user)
{ {
@ -119,12 +121,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
Text = user?.Username Text = user?.Username
}, },
new OsuSpriteText userRankText = new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14), Font = OsuFont.GetFont(size: 14),
Text = user?.CurrentModeRank != null ? $"#{user.CurrentModeRank}" : string.Empty
} }
} }
}, },
@ -162,6 +163,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50; const double fade_time = 50;
var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance();
var currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if (Room.Host?.Equals(User) == true) if (Room.Host?.Equals(User) == true)
@ -171,11 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
Schedule(() => Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList());
{
var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance();
userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList();
});
} }
public MenuItem[] ContextMenuItems public MenuItem[] ContextMenuItems

View File

@ -2,14 +2,19 @@
// 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;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Screens.OnlinePlay namespace osu.Game.Screens.OnlinePlay
{ {
/// <summary>
/// A <see cref="CompositeDrawable"/> that exposes bindables for <see cref="Room"/> properties.
/// </summary>
public class OnlinePlayComposite : CompositeDrawable public class OnlinePlayComposite : CompositeDrawable
{ {
[Resolved(typeof(Room))] [Resolved(typeof(Room))]
@ -53,5 +58,23 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))] [Resolved(typeof(Room))]
protected Bindable<TimeSpan?> Duration { get; private set; } protected Bindable<TimeSpan?> Duration { get; private set; }
/// <summary>
/// The currently selected item in the <see cref="RoomSubScreen"/>, or the first item from <see cref="Playlist"/>
/// if this <see cref="OnlinePlayComposite"/> is not within a <see cref="RoomSubScreen"/>.
/// </summary>
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
protected override void LoadComplete()
{
base.LoadComplete();
Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true);
}
protected virtual void UpdateSelectedItem()
{
SelectedItem.Value = Playlist.FirstOrDefault();
}
} }
} }

View File

@ -28,7 +28,7 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay namespace osu.Game.Screens.OnlinePlay
{ {
[Cached] [Cached]
public abstract class OnlinePlayScreen : OsuScreen public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack
{ {
public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true;
@ -355,5 +355,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override double TransformDuration => 200; protected override double TransformDuration => 200;
} }
} }
ScreenStack IHasSubScreenStack.SubScreenStack => screenStack;
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -31,7 +32,12 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room), nameof(Room.Playlist))] [Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; } protected BindableList<PlaylistItem> Playlist { get; private set; }
private readonly Bindable<IReadOnlyList<Mod>> freeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
[CanBeNull]
[Resolved(CanBeNull = true)]
private IBindable<PlaylistItem> selectedItem { get; set; }
private readonly FreeModSelectOverlay freeModSelectOverlay; private readonly FreeModSelectOverlay freeModSelectOverlay;
private WorkingBeatmap initialBeatmap; private WorkingBeatmap initialBeatmap;
@ -45,7 +51,7 @@ namespace osu.Game.Screens.OnlinePlay
freeModSelectOverlay = new FreeModSelectOverlay freeModSelectOverlay = new FreeModSelectOverlay
{ {
SelectedMods = { BindTarget = freeMods }, SelectedMods = { BindTarget = FreeMods },
IsValidMod = IsValidFreeMod, IsValidMod = IsValidFreeMod,
}; };
} }
@ -66,15 +72,15 @@ namespace osu.Game.Screens.OnlinePlay
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods. // Similarly, freeMods is currently empty but should only contain the allowed mods.
Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>(); Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>(); FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
Ruleset.BindValueChanged(onRulesetChanged); Ruleset.BindValueChanged(onRulesetChanged);
} }
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset) private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
{ {
freeMods.Value = Array.Empty<Mod>(); FreeMods.Value = Array.Empty<Mod>();
} }
protected sealed override bool OnStart() protected sealed override bool OnStart()
@ -90,7 +96,7 @@ namespace osu.Game.Screens.OnlinePlay
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.Clear(); item.AllowedMods.Clear();
item.AllowedMods.AddRange(freeMods.Value.Select(m => m.CreateCopy())); item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy()));
SelectItem(item); SelectItem(item);
return true; return true;
@ -133,7 +139,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons()
{ {
var buttons = base.CreateFooterButtons().ToList(); var buttons = base.CreateFooterButtons().ToList();
buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = freeMods }, freeModSelectOverlay)); buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = FreeMods }, freeModSelectOverlay));
return buttons; return buttons;
} }

View File

@ -13,7 +13,9 @@ using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users; using osu.Game.Users;
using osuTK;
using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer; using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer;
namespace osu.Game.Screens.OnlinePlay.Playlists namespace osu.Game.Screens.OnlinePlay.Playlists
@ -27,6 +29,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved(typeof(Room), nameof(Room.RoomID))] [Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<long?> roomId { get; set; } private Bindable<long?> roomId { get; set; }
[Resolved(typeof(Room), nameof(Room.Playlist))]
private BindableList<PlaylistItem> playlist { get; set; }
private MatchSettingsOverlay settingsOverlay; private MatchSettingsOverlay settingsOverlay;
private MatchLeaderboard leaderboard; private MatchLeaderboard leaderboard;
@ -117,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new DrawableRoomPlaylistWithResults new DrawableRoomPlaylistWithResults
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Playlist }, Items = { BindTarget = playlist },
SelectedItem = { BindTarget = SelectedItem }, SelectedItem = { BindTarget = SelectedItem },
RequestShowResults = item => RequestShowResults = item =>
{ {
@ -140,13 +145,55 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Content = new[] Content = new[]
{ {
new Drawable[] { new OverlinedHeader("Leaderboard"), }, new[]
{
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 10 },
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new PurpleTriangleButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DisplayUnrankedText = false,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
}
}
},
},
new Drawable[]
{
new OverlinedHeader("Leaderboard")
},
new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, }, new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, },
new Drawable[] { new OverlinedHeader("Chat"), }, new Drawable[] { new OverlinedHeader("Chat"), },
new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } }
}, },
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
new Dimension(), new Dimension(),
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
@ -222,7 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
// Set the first playlist item. // Set the first playlist item.
// This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). // This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()).
Schedule(() => SelectedItem.Value = Playlist.FirstOrDefault()); Schedule(() => SelectedItem.Value = playlist.FirstOrDefault());
} }
}, true); }, true);
} }

View File

@ -56,6 +56,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
item.RequiredMods.Clear(); item.RequiredMods.Clear();
item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy()));
item.AllowedMods.Clear();
item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy()));
} }
} }
} }

View File

@ -0,0 +1,38 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match;
namespace osu.Game.Screens.OnlinePlay
{
/// <summary>
/// An <see cref="OnlinePlayComposite"/> with additional logic tracking the currently-selected <see cref="PlaylistItem"/> inside a <see cref="RoomSubScreen"/>.
/// </summary>
public class RoomSubScreenComposite : OnlinePlayComposite
{
[Resolved]
private IBindable<PlaylistItem> subScreenSelectedItem { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
subScreenSelectedItem.BindValueChanged(_ => UpdateSelectedItem(), true);
}
protected override void UpdateSelectedItem()
{
if (RoomID.Value == null)
{
// If the room hasn't been created yet, fall-back to the base logic.
base.UpdateSelectedItem();
return;
}
SelectedItem.Value = subScreenSelectedItem.Value;
}
}
}

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -16,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD
{ {
private readonly Cached sorting = new Cached(); private readonly Cached sorting = new Cached();
public Bindable<bool> Expanded = new Bindable<bool>();
public GameplayLeaderboard() public GameplayLeaderboard()
{ {
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
@ -47,8 +50,7 @@ namespace osu.Game.Screens.Play.HUD
{ {
var drawable = new GameplayLeaderboardScore(user, isTracked) var drawable = new GameplayLeaderboardScore(user, isTracked)
{ {
Anchor = Anchor.TopRight, Expanded = { BindTarget = Expanded },
Origin = Anchor.TopRight,
}; };
base.Add(drawable); base.Add(drawable);

View File

@ -20,16 +20,33 @@ namespace osu.Game.Screens.Play.HUD
{ {
public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore
{ {
public const float EXTENDED_WIDTH = 255f; public const float EXTENDED_WIDTH = regular_width + top_player_left_width_extension;
private const float regular_width = 235f; private const float regular_width = 235f;
// a bit hand-wavy, but there's a lot of hard-coded paddings in each of the grid's internals.
private const float compact_width = 77.5f;
private const float top_player_left_width_extension = 20f;
public const float PANEL_HEIGHT = 35f; public const float PANEL_HEIGHT = 35f;
public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear; public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear;
private const float panel_shear = 0.15f; private const float panel_shear = 0.15f;
private const float rank_text_width = 35f;
private const float score_components_width = 85f;
private const float avatar_size = 25f;
private const double panel_transition_duration = 500;
private const double text_transition_duration = 200;
public Bindable<bool> Expanded = new Bindable<bool>();
private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText;
public BindableDouble TotalScore { get; } = new BindableDouble(); public BindableDouble TotalScore { get; } = new BindableDouble();
@ -63,8 +80,15 @@ namespace osu.Game.Screens.Play.HUD
private readonly bool trackedPlayer; private readonly bool trackedPlayer;
private Container mainFillContainer; private Container mainFillContainer;
private Box centralFill; private Box centralFill;
private Container backgroundPaddingAdjustContainer;
private GridContainer gridContainer;
private Container scoreComponents;
/// <summary> /// <summary>
/// Creates a new <see cref="GameplayLeaderboardScore"/>. /// Creates a new <see cref="GameplayLeaderboardScore"/>.
/// </summary> /// </summary>
@ -75,7 +99,8 @@ namespace osu.Game.Screens.Play.HUD
User = user; User = user;
this.trackedPlayer = trackedPlayer; this.trackedPlayer = trackedPlayer;
Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT); AutoSizeAxes = Axes.X;
Height = PANEL_HEIGHT;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -85,147 +110,167 @@ namespace osu.Game.Screens.Play.HUD
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
mainFillContainer = new Container new Container
{ {
Width = regular_width, AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Anchor = Anchor.TopRight, Margin = new MarginPadding { Left = top_player_left_width_extension },
Origin = Anchor.TopRight, Children = new Drawable[]
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
Child = new Box
{ {
Alpha = 0.5f, backgroundPaddingAdjustContainer = new Container
RelativeSizeAxes = Axes.Both,
}
},
new GridContainer
{
Width = regular_width,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 35f),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 85f),
},
Content = new[]
{
new Drawable[]
{ {
positionText = new OsuSpriteText RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 }, mainFillContainer = new Container
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold),
Shadow = false,
},
new Container
{
Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 },
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
new Container Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
Children = new Drawable[]
{ {
Masking = true, new Box
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
RelativeSizeAxes = Axes.Both,
Children = new[]
{ {
centralFill = new Box Alpha = 0.5f,
{ RelativeSizeAxes = Axes.Both,
Alpha = 0.5f, },
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("3399cc"),
},
}
},
new FillFlowContainer
{
Padding = new MarginPadding { Left = SHEAR_WIDTH },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4f, 0f),
Children = new Drawable[]
{
avatarContainer = new CircularContainer
{
Masking = true,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(25f),
Children = new Drawable[]
{
new Box
{
Name = "Placeholder while avatar loads",
Alpha = 0.3f,
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray4,
}
}
},
usernameText = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 0.6f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Text = User?.Username,
Truncate = true,
Shadow = false,
}
}
},
}
},
new Container
{
Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f },
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Children = new Drawable[]
{
scoreText = new OsuSpriteText
{
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
},
accuracyText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1f, 0f),
Shadow = false,
},
comboText = new OsuSpriteText
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
}, },
}, },
} }
},
gridContainer = new GridContainer
{
RelativeSizeAxes = Axes.Y,
Width = compact_width, // will be updated by expanded state.
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, rank_text_width),
new Dimension(),
new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width),
},
Content = new[]
{
new Drawable[]
{
positionText = new OsuSpriteText
{
Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold),
Shadow = false,
},
new Container
{
Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 },
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
Masking = true,
CornerRadius = 5f,
Shear = new Vector2(panel_shear, 0f),
RelativeSizeAxes = Axes.Both,
Children = new[]
{
centralFill = new Box
{
Alpha = 0.5f,
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("3399cc"),
},
}
},
new FillFlowContainer
{
Padding = new MarginPadding { Left = SHEAR_WIDTH },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4f, 0f),
Children = new Drawable[]
{
avatarContainer = new CircularContainer
{
Masking = true,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(avatar_size),
Children = new Drawable[]
{
new Box
{
Name = "Placeholder while avatar loads",
Alpha = 0.3f,
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray4,
}
}
},
usernameText = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 0.6f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Text = User?.Username,
Truncate = true,
Shadow = false,
}
}
},
}
},
scoreComponents = new Container
{
Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f },
AlwaysPresent = true, // required to smoothly animate autosize after hidden early.
Masking = true,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Children = new Drawable[]
{
scoreText = new OsuSpriteText
{
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
},
accuracyText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1f, 0f),
Shadow = false,
},
comboText = new OsuSpriteText
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Spacing = new Vector2(-1f, 0f),
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true),
Shadow = false,
},
},
}
}
}
} }
} }
} },
}; };
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
@ -241,18 +286,43 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete(); base.LoadComplete();
updateState(); updateState();
Expanded.BindValueChanged(changeExpandedState, true);
FinishTransforms(true); FinishTransforms(true);
} }
private const double panel_transition_duration = 500; private void changeExpandedState(ValueChangedEvent<bool> expanded)
{
scoreComponents.ClearTransforms();
if (expanded.NewValue)
{
gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.ResizeWidthTo(score_components_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint);
usernameText.FadeIn(panel_transition_duration, Easing.OutQuint);
}
else
{
gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint);
usernameText.FadeOut(text_transition_duration, Easing.OutQuint);
}
}
private void updateState() private void updateState()
{ {
bool widthExtension = false;
if (HasQuit.Value) if (HasQuit.Value)
{ {
// we will probably want to display this in a better way once we have a design. // we will probably want to display this in a better way once we have a design.
// and also show states other than quit. // and also show states other than quit.
mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic);
panelColour = Color4.Gray; panelColour = Color4.Gray;
textColour = Color4.White; textColour = Color4.White;
return; return;
@ -260,22 +330,29 @@ namespace osu.Game.Screens.Play.HUD
if (scorePosition == 1) if (scorePosition == 1)
{ {
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); widthExtension = true;
panelColour = Color4Extensions.FromHex("7fcc33"); panelColour = Color4Extensions.FromHex("7fcc33");
textColour = Color4.White; textColour = Color4.White;
} }
else if (trackedPlayer) else if (trackedPlayer)
{ {
mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); widthExtension = true;
panelColour = Color4Extensions.FromHex("ffd966"); panelColour = Color4Extensions.FromHex("ffd966");
textColour = Color4Extensions.FromHex("2e576b"); textColour = Color4Extensions.FromHex("2e576b");
} }
else else
{ {
mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic);
panelColour = Color4Extensions.FromHex("3399cc"); panelColour = Color4Extensions.FromHex("3399cc");
textColour = Color4.White; textColour = Color4.White;
} }
this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic);
}
public float SizeContainerLeftPadding
{
get => backgroundPaddingAdjustContainer.Padding.Left;
set => backgroundPaddingAdjustContainer.Padding = new MarginPadding { Left = value };
} }
private Color4 panelColour private Color4 panelColour
@ -287,8 +364,6 @@ namespace osu.Game.Screens.Play.HUD
} }
} }
private const double text_transition_duration = 200;
private Color4 textColour private Color4 textColour
{ {
set set

View File

@ -339,7 +339,7 @@ namespace osu.Game.Screens.Play
{ {
HoldToQuit = HoldToQuit =
{ {
Action = performUserRequestedExit, Action = () => PerformExit(true),
IsPaused = { BindTarget = GameplayClockContainer.IsPaused } IsPaused = { BindTarget = GameplayClockContainer.IsPaused }
}, },
PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } },
@ -363,14 +363,14 @@ namespace osu.Game.Screens.Play
FailOverlay = new FailOverlay FailOverlay = new FailOverlay
{ {
OnRetry = Restart, OnRetry = Restart,
OnQuit = performUserRequestedExit, OnQuit = () => PerformExit(true),
}, },
PauseOverlay = new PauseOverlay PauseOverlay = new PauseOverlay
{ {
OnResume = Resume, OnResume = Resume,
Retries = RestartCount, Retries = RestartCount,
OnRetry = Restart, OnRetry = Restart,
OnQuit = performUserRequestedExit, OnQuit = () => PerformExit(true),
}, },
new HotkeyExitOverlay new HotkeyExitOverlay
{ {
@ -379,7 +379,7 @@ namespace osu.Game.Screens.Play
if (!this.IsCurrentScreen()) return; if (!this.IsCurrentScreen()) return;
fadeOut(true); fadeOut(true);
PerformExit(true); PerformExit(false);
}, },
}, },
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
@ -432,9 +432,9 @@ namespace osu.Game.Screens.Play
if (gameActive.Value == false) if (gameActive.Value == false)
{ {
if (canPause) bool paused = Pause();
Pause();
else if (!paused)
Scheduler.AddOnce(updatePauseOnFocusLostState); Scheduler.AddOnce(updatePauseOnFocusLostState);
} }
} }
@ -483,23 +483,47 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// Exits the <see cref="Player"/>. /// Exits the <see cref="Player"/>.
/// </summary> /// </summary>
/// <param name="userRequested"> /// <param name="showDialogFirst">
/// Whether the exit is requested by the user, or a higher-level game component. /// Whether the pause or fail dialog should be shown before performing an exit.
/// Pausing is allowed only in the former case. /// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead.
/// </param> /// </param>
protected void PerformExit(bool userRequested) protected void PerformExit(bool showDialogFirst)
{ {
// if a restart has been requested, cancel any pending completion (user has shown intent to restart). // if a restart has been requested, cancel any pending completion (user has shown intent to restart).
completionProgressDelegate?.Cancel(); completionProgressDelegate?.Cancel();
ValidForResume = false; // there is a chance that the exit was performed after the transition to results has started.
// we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen())
{
ValidForResume = false;
this.MakeCurrent();
return;
}
if (!this.IsCurrentScreen()) return; bool pauseOrFailDialogVisible =
PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible;
if (userRequested) if (showDialogFirst && !pauseOrFailDialogVisible)
performUserRequestedExit(); {
else // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
this.Exit(); if (ValidForResume && HasFailed)
{
failAnimation.FinishTransforms(true);
return;
}
// there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred.
if (pausingSupportedByCurrentState)
{
// in the case a dialog needs to be shown, attempt to pause and show it.
// this may fail (see internal checks in Pause()) but the fail cases are temporary, so don't fall through to Exit().
Pause();
return;
}
}
this.Exit();
} }
private void performUserRequestedSkip() private void performUserRequestedSkip()
@ -513,20 +537,6 @@ namespace osu.Game.Screens.Play
updateSampleDisabledState(); updateSampleDisabledState();
} }
private void performUserRequestedExit()
{
if (ValidForResume && HasFailed && !FailOverlay.IsPresent)
{
failAnimation.FinishTransforms(true);
return;
}
if (canPause)
Pause();
else
this.Exit();
}
/// <summary> /// <summary>
/// Restart gameplay via a parent <see cref="PlayerLoader"/>. /// Restart gameplay via a parent <see cref="PlayerLoader"/>.
/// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks> /// <remarks>This can be called from a child screen in order to trigger the restart process.</remarks>
@ -543,10 +553,7 @@ namespace osu.Game.Screens.Play
sampleRestart?.Play(); sampleRestart?.Play();
RestartRequested?.Invoke(); RestartRequested?.Invoke();
if (this.IsCurrentScreen()) PerformExit(false);
PerformExit(true);
else
this.MakeCurrent();
} }
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
@ -675,15 +682,18 @@ namespace osu.Game.Screens.Play
protected bool PauseCooldownActive => protected bool PauseCooldownActive =>
lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown;
private bool canPause => /// <summary>
/// A set of conditionals which defines whether the current game state and configuration allows for
/// pausing to be attempted via <see cref="Pause"/>. If false, the game should generally exit if a user pause
/// is attempted.
/// </summary>
private bool pausingSupportedByCurrentState =>
// must pass basic screen conditions (beatmap loaded, instance allows pause) // must pass basic screen conditions (beatmap loaded, instance allows pause)
LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume
// replays cannot be paused and exit immediately // replays cannot be paused and exit immediately
&& !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.HasReplayLoaded.Value
// cannot pause if we are already in a fail state // cannot pause if we are already in a fail state
&& !HasFailed && !HasFailed;
// cannot pause if already paused (or in a cooldown state) unless we are in a resuming state.
&& (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !PauseCooldownActive));
private bool canResume => private bool canResume =>
// cannot resume from a non-paused state // cannot resume from a non-paused state
@ -693,9 +703,12 @@ namespace osu.Game.Screens.Play
// already resuming // already resuming
&& !IsResuming; && !IsResuming;
public void Pause() public bool Pause()
{ {
if (!canPause) return; if (!pausingSupportedByCurrentState) return false;
if (!IsResuming && PauseCooldownActive)
return false;
if (IsResuming) if (IsResuming)
{ {
@ -706,6 +719,7 @@ namespace osu.Game.Screens.Play
GameplayClockContainer.Stop(); GameplayClockContainer.Stop();
PauseOverlay.Show(); PauseOverlay.Show();
lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime;
return true;
} }
public void Resume() public void Resume()
@ -814,14 +828,6 @@ namespace osu.Game.Screens.Play
return true; return true;
} }
// ValidForResume is false when restarting
if (ValidForResume)
{
if (PauseCooldownActive && !GameplayClockContainer.IsPaused.Value)
// still want to block if we are within the cooldown period and not already paused.
return true;
}
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
// as we are no longer the current screen, we cannot guarantee the track is still usable. // as we are no longer the current screen, we cannot guarantee the track is still usable.
GameplayClockContainer?.StopUsingBeatmapClock(); GameplayClockContainer?.StopUsingBeatmapClock();

View File

@ -44,7 +44,7 @@ namespace osu.Game.Screens.Select
Sort = sortMode.Value, Sort = sortMode.Value,
AllowConvertedBeatmaps = showConverted.Value, AllowConvertedBeatmaps = showConverted.Value,
Ruleset = ruleset.Value, Ruleset = ruleset.Value,
Collection = collectionDropdown?.Current.Value.Collection Collection = collectionDropdown?.Current.Value?.Collection
}; };
if (!minimumStars.IsDefault) if (!minimumStars.IsDefault)

View File

@ -17,7 +17,7 @@ namespace osu.Game.Skinning
/// <summary> /// <summary>
/// A sample corresponding to an <see cref="ISampleInfo"/> that supports being pooled and responding to skin changes. /// A sample corresponding to an <see cref="ISampleInfo"/> that supports being pooled and responding to skin changes.
/// </summary> /// </summary>
public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent public class PoolableSkinnableSample : SkinReloadableDrawable, IAdjustableAudioComponent
{ {
/// <summary> /// <summary>
/// The currently-loaded <see cref="DrawableSample"/>. /// The currently-loaded <see cref="DrawableSample"/>.
@ -165,9 +165,13 @@ namespace osu.Game.Skinning
public BindableNumber<double> Tempo => sampleContainer.Tempo; public BindableNumber<double> Tempo => sampleContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); public void BindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.BindAdjustments(component);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); public void UnbindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.UnbindAdjustments(component);
public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type); public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type);

View File

@ -176,10 +176,20 @@ namespace osu.Game.Skinning
public BindableNumber<double> Tempo => samplesContainer.Tempo; public BindableNumber<double> Tempo => samplesContainer.Tempo;
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) public void BindAdjustments(IAggregateAudioAdjustment component)
{
samplesContainer.BindAdjustments(component);
}
public void UnbindAdjustments(IAggregateAudioAdjustment component)
{
samplesContainer.UnbindAdjustments(component);
}
public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
=> samplesContainer.AddAdjustment(type, adjustBindable); => samplesContainer.AddAdjustment(type, adjustBindable);
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
=> samplesContainer.RemoveAdjustment(type, adjustBindable); => samplesContainer.RemoveAdjustment(type, adjustBindable);
public void RemoveAllAdjustments(AdjustableProperty type) public void RemoveAllAdjustments(AdjustableProperty type)
@ -192,6 +202,14 @@ namespace osu.Game.Skinning
public bool IsPlayed => samplesContainer.Any(s => s.Played); public bool IsPlayed => samplesContainer.Any(s => s.Played);
public IBindable<double> AggregateVolume => samplesContainer.AggregateVolume;
public IBindable<double> AggregateBalance => samplesContainer.AggregateBalance;
public IBindable<double> AggregateFrequency => samplesContainer.AggregateFrequency;
public IBindable<double> AggregateTempo => samplesContainer.AggregateTempo;
#endregion #endregion
} }
} }

View File

@ -2,10 +2,13 @@
// 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.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Online.API.Requests;
namespace osu.Game.Users namespace osu.Game.Users
{ {
@ -178,6 +181,10 @@ namespace osu.Game.Users
private UserStatistics statistics; private UserStatistics statistics;
/// <summary>
/// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> response).
/// Otherwise empty.
/// </summary>
[JsonProperty(@"statistics")] [JsonProperty(@"statistics")]
public UserStatistics Statistics public UserStatistics Statistics
{ {
@ -228,14 +235,14 @@ namespace osu.Game.Users
[JsonProperty("replays_watched_counts")] [JsonProperty("replays_watched_counts")]
public UserHistoryCount[] ReplaysWatchedCounts; public UserHistoryCount[] ReplaysWatchedCounts;
public class UserHistoryCount /// <summary>
{ /// All user statistics per ruleset's short name (in the case of a <see cref="GetUsersRequest"/> response).
[JsonProperty("start_date")] /// Otherwise empty. Can be altered for testing purposes.
public DateTime Date; /// </summary>
// todo: this should likely be moved to a separate UserCompact class at some point.
[JsonProperty("count")] [JsonProperty("statistics_rulesets")]
public long Count; [CanBeNull]
} public Dictionary<string, UserStatistics> RulesetsStatistics { get; set; }
public override string ToString() => Username; public override string ToString() => Username;
@ -249,6 +256,14 @@ namespace osu.Game.Users
Id = 0 Id = 0
}; };
public bool Equals(User other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Id == other.Id;
}
public enum PlayStyle public enum PlayStyle
{ {
[Description("Keyboard")] [Description("Keyboard")]
@ -264,12 +279,13 @@ namespace osu.Game.Users
Touch, Touch,
} }
public bool Equals(User other) public class UserHistoryCount
{ {
if (ReferenceEquals(null, other)) return false; [JsonProperty("start_date")]
if (ReferenceEquals(this, other)) return true; public DateTime Date;
return Id == other.Id; [JsonProperty("count")]
public long Count;
} }
} }
} }

View File

@ -26,17 +26,24 @@ namespace osu.Game.Users
public int Progress; public int Progress;
} }
[JsonProperty(@"pp")] [JsonProperty(@"global_rank")]
public decimal? PP; public int? GlobalRank;
[JsonProperty(@"pp_rank")] // the API sometimes only returns this value in condensed user responses public int? CountryRank;
private int? rank
{
set => Ranks.Global = value;
}
[JsonProperty(@"rank")] [JsonProperty(@"rank")]
public UserRanks Ranks; private UserRanks ranks
{
// eventually that will also become an own json property instead of reading from a `rank` object.
// see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53.
set => CountryRank = value.Country;
}
// populated via User model, as that's where the data currently lives.
public RankHistoryData RankHistory;
[JsonProperty(@"pp")]
public decimal? PP;
[JsonProperty(@"ranked_score")] [JsonProperty(@"ranked_score")]
public long RankedScore; public long RankedScore;
@ -113,15 +120,12 @@ namespace osu.Game.Users
} }
} }
public struct UserRanks #pragma warning disable 649
private struct UserRanks
{ {
[JsonProperty(@"global")]
public int? Global;
[JsonProperty(@"country")] [JsonProperty(@"country")]
public int? Country; public int? Country;
} }
#pragma warning restore 649
public RankHistoryData RankHistory;
} }
} }

View File

@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" /> <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.215.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.222.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="Sentry" Version="3.0.1" /> <PackageReference Include="Sentry" Version="3.0.1" />
<PackageReference Include="SharpCompress" Version="0.27.1" /> <PackageReference Include="SharpCompress" Version="0.27.1" />

View File

@ -70,7 +70,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.215.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.222.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
</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) -->
@ -91,7 +91,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="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.215.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.222.0" />
<PackageReference Include="SharpCompress" Version="0.27.1" /> <PackageReference Include="SharpCompress" Version="0.27.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />