From 8b69aa9fdbdbcc6610d2cc5d6c3994e8ad5d242a Mon Sep 17 00:00:00 2001 From: Krzysztof Gutkowski Date: Sat, 4 Apr 2026 15:05:28 +0200 Subject: [PATCH] Display beatmap state in `RankedPlayUserDisplay` (#37188) This has gone through a few iterations, and eventually ended up as a simple text percentage display next to the username. I feel that adding another progress bar right next to the big healthbar would make things too cluttered, and trying to move the beatmap state elsewhere would make it too disconnected from the players that are potentially downloading a beatmap. I considered making the local user fetch download progress data using `BeatmapDownloadTracker` instead of relying on `BeatmapAvailability` in order to get more frequent updates, but that would add a lot of extra complexity for little gain IMO. [Screencast_20260403_095644.webm](https://github.com/user-attachments/assets/85fbd4b8-6b5c-41d2-b29b-c93885f73bb3) --- .../TestSceneRankedPlayUserDisplay.cs | 33 +++++++- .../Components/RankedPlayUserDisplay.cs | 83 +++++++++++++++++-- 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs index f7cc9885c7..de784157e9 100644 --- a/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs +++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayUserDisplay.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay; using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components; using osu.Game.Tests.Visual.Multiplayer; @@ -20,11 +22,19 @@ namespace osu.Game.Tests.Visual.RankedPlay Value = 1_000_000, }; + public TestSceneRankedPlayUserDisplay() + { + AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value); + } + public override void SetUpSteps() { base.SetUpSteps(); - AddStep("add display", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue) + AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.RankedPlay))); + WaitForJoined(); + + AddStep("add display", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Blue) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -36,7 +46,7 @@ namespace osu.Game.Tests.Visual.RankedPlay [Test] public void TesUserDisplay() { - AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue) + AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Blue) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -44,15 +54,30 @@ namespace osu.Game.Tests.Visual.RankedPlay Health = { BindTarget = health } }); - AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Red) + AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(1001, Anchor.BottomLeft, RankedPlayColourScheme.Red) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(256, 72), Health = { BindTarget = health } }); + } - AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value); + [Test] + public void TestBeatmapState() + { + float progress = 0; + + AddStep("set unavailable", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); + AddStep("set downloading", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress = 0))); + AddUntilStep("increment progress", () => + { + progress += RNG.NextSingle(0.1f); + MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress)); + return progress >= 1; + }); + AddStep("set to importing", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); + AddStep("set to available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); } } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs index 0195c47945..aa5b303aad 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/RankedPlay/Components/RankedPlayUserDisplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -17,7 +18,10 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; @@ -42,6 +46,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components private BufferedContainer grayScaleContainer = null!; + private OsuSpriteText beatmapState = null!; + + private BeatmapAvailability availability = BeatmapAvailability.Unknown(); + + [Resolved] + private MultiplayerClient client { get; set; } = null!; + [Resolved] private RankedPlayCornerPiece? cornerPiece { get; set; } @@ -61,6 +72,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components ? -OsuGame.SHEAR : OsuGame.SHEAR; + var beatmapStateAnchor = (contentAnchor & Anchor.x0) != 0 + ? Anchor.CentreLeft + : Anchor.CentreRight; + InternalChildren = [ new CircularContainer @@ -103,15 +118,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components Anchor = contentAnchor, Origin = contentAnchor, }, - new OsuSpriteText + new FillFlowContainer { - Name = "Username", - Text = user.Username, + Name = "Username/beatmap state container", + AutoSizeAxes = Axes.Both, Anchor = contentAnchor, Origin = contentAnchor, + Direction = FillDirection.Horizontal, Padding = new MarginPadding { Horizontal = 4, Vertical = 6 }, - Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold), - UseFullGlyphHeight = false, + Spacing = new Vector2(5, 0), + Children = + [ + new OsuSpriteText + { + Name = "Username", + Text = user.Username, + Anchor = contentAnchor, + Origin = contentAnchor, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold), + UseFullGlyphHeight = false, + }, + beatmapState = new OsuSpriteText + { + Anchor = beatmapStateAnchor, + Origin = beatmapStateAnchor, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + }, + ], }, ] } @@ -129,6 +162,46 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components grayScaleContainer.GrayscaleTo(e.NewValue <= 0 ? 1 : 0, 300); cornerPiece?.OnHealthChanged(e.NewValue); }); + + client.RoomUpdated += onRoomUpdated; + } + + private void onRoomUpdated() + { + var user = client.Room?.Users.SingleOrDefault(u => u.UserID == userId); + + if (user == null || availability == user.BeatmapAvailability) + return; + + availability = user.BeatmapAvailability; + + if (availability.State is DownloadState.NotDownloaded or DownloadState.Downloading or DownloadState.Importing) + beatmapState.FadeIn(50); + else + beatmapState.FadeOut(50); + + switch (availability.State) + { + case DownloadState.NotDownloaded: + beatmapState.Text = "Missing Beatmap"; + break; + + case DownloadState.Downloading: + double progress = Math.Clamp(availability.DownloadProgress ?? 0, 0, 1); + beatmapState.Text = $"Downloading... ({progress:P0})"; + break; + + case DownloadState.Importing: + beatmapState.Text = "Importing..."; + break; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + client.RoomUpdated -= onRoomUpdated; } public partial class HealthBar : CompositeDrawable