From 52fbd9e7969f3d21c5c945da323ca9e6fe7be2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Apr 2025 13:08:42 +0200 Subject: [PATCH] Implement local user position display for multiplayer --- .../TestSceneMultiplayerPositionDisplay.cs | 93 +++++++++++++++++++ .../Multiplayer/MultiplayerPositionDisplay.cs | 71 ++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs new file mode 100644 index 0000000000..34e9080db3 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Online.API; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Tests.Gameplay; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public partial class TestSceneMultiplayerPositionDisplay : OsuTestScene + { + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Test] + public void TestAppearance() + { + TestGameplayLeaderboardProvider leaderboard = null!; + MultiplayerPositionDisplay display = null!; + GameplayState gameplayState = null!; + + AddStep("create content", () => + { + leaderboard = new TestGameplayLeaderboardProvider(); + Children = new Drawable[] + { + leaderboard, + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(IGameplayLeaderboardProvider), leaderboard), + (typeof(GameplayState), gameplayState = TestGameplayState.Create(new OsuRuleset())) + ], + Child = display = new MultiplayerPositionDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; + }); + AddSliderStep("set score position", 1, 100, 50, r => + { + if (leaderboard.IsNotNull() && leaderboard.Score.IsNotNull()) + leaderboard.Score.Position.Value = r; + }); + AddStep("unset position", () => leaderboard.Score.Position.Value = null); + + AddStep("toggle leaderboard on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); + AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1)); + + AddStep("toggle leaderboard off", () => config.SetValue(OsuSetting.GameplayLeaderboard, false)); + AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); + + AddStep("enter break", () => ((Bindable)gameplayState.PlayingState).Value = LocalUserPlayingState.Break); + AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1)); + + AddStep("exit break", () => ((Bindable)gameplayState.PlayingState).Value = LocalUserPlayingState.Playing); + AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); + + AddStep("toggle leaderboard on", () => config.SetValue(OsuSetting.GameplayLeaderboard, true)); + AddUntilStep("display visible", () => display.Alpha, () => Is.EqualTo(1)); + + AddStep("change local user", () => ((DummyAPIAccess)API).LocalUser.Value = new GuestUser()); + AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); + } + + private partial class TestGameplayLeaderboardProvider : Component, IGameplayLeaderboardProvider + { + public GameplayLeaderboardScore Score { get; private set; } = null!; + + IBindableList IGameplayLeaderboardProvider.Scores => scores; + private readonly BindableList scores = new BindableList(); + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + scores.Add(Score = new GameplayLeaderboardScore(api.LocalUser.Value, true, new BindableLong())); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs new file mode 100644 index 0000000000..af847a7b51 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select.Leaderboards; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public partial class MultiplayerPositionDisplay : CompositeDrawable + { + private readonly IBindable user = new Bindable(); + private readonly IBindableList scores = new BindableList(); + private readonly BindableBool showLeaderboard = new BindableBool(); + private readonly IBindable localUserPlayingState = new Bindable(); + + private readonly Bindable position = new Bindable(); + + private OsuSpriteText positionText = null!; + + [BackgroundDependencyLoader] + private void load(IGameplayLeaderboardProvider leaderboardProvider, IAPIProvider api, OsuConfigManager configManager, GameplayState gameplayState) + { + scores.BindTo(leaderboardProvider.Scores); + user.BindTo(api.LocalUser); + configManager.BindWith(OsuSetting.GameplayLeaderboard, showLeaderboard); + localUserPlayingState.BindTo(gameplayState.PlayingState); + + AutoSizeAxes = Axes.Both; + InternalChild = positionText = new OsuSpriteText + { + Alpha = 0.5f, + Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light), + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + user.BindValueChanged(_ => updateState()); + scores.BindCollectionChanged((_, __) => updateState()); + showLeaderboard.BindValueChanged(_ => updateState()); + localUserPlayingState.BindValueChanged(_ => updateState(), true); + + position.BindValueChanged(_ => positionText.Text = position.Value != null ? $@"#{position.Value.Value:N0}" : "-", true); + } + + private void updateState() + { + position.UnbindBindings(); + + var userScore = scores.SingleOrDefault(s => s.User.Equals(user.Value)); + if (userScore != null) + position.BindTo(userScore.Position); + else + position.Value = null; + + Alpha = userScore != null && (showLeaderboard.Value || localUserPlayingState.Value == LocalUserPlayingState.Break) ? 1 : 0; + } + } +}