From 4d051818a152573834a0e859994f25c83bda1b7a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:30:53 +0900 Subject: [PATCH 1/4] Add base class for all realtime multiplayer classes --- .../RealtimeRoomComposite.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs new file mode 100644 index 0000000000..e6d1274316 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Game.Online.RealtimeMultiplayer; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public abstract class RealtimeRoomComposite : MultiplayerComposite + { + [CanBeNull] + protected MultiplayerRoom Room => Client.Room; + + [Resolved] + protected StatefulMultiplayerClient Client { get; private set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Client.RoomChanged += OnRoomChanged; + OnRoomChanged(); + } + + protected virtual void OnRoomChanged() + { + } + + protected override void Dispose(bool isDisposing) + { + if (Client != null) + Client.RoomChanged -= OnRoomChanged; + + base.Dispose(isDisposing); + } + } +} From 1e5c32410ad572883295e0f8e47e90704ef4593e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:37:47 +0900 Subject: [PATCH 2/4] Add the realtime multiplayer participants list --- .../TestSceneParticipantsList.cs | 96 +++++++++ .../Participants/ParticipantPanel.cs | 187 ++++++++++++++++++ .../Participants/ParticipantsList.cs | 55 ++++++ .../Participants/ReadyMark.cs | 51 +++++ 4 files changed, 389 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs new file mode 100644 index 0000000000..ee6bbc4ecd --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs @@ -0,0 +1,96 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneParticipantsList : RealtimeMultiplayerTestScene + { + [SetUp] + public new void Setup() => Schedule(() => + { + Child = new ParticipantsList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(380, 0.7f) + }; + }); + + [Test] + public void TestAddUser() + { + AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); + + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); + } + + [Test] + public void TestRemoveUser() + { + User secondUser = null; + + AddStep("add a user", () => + { + Client.AddUser(secondUser = new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + }); + + AddStep("remove host", () => Client.RemoveUser(API.LocalUser.Value)); + + AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser); + } + + [Test] + public void TestToggleReadyState() + { + AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + + AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); + + AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + } + + [Test] + public void TestCrownChangesStateWhenHostTransferred() + { + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddUntilStep("first user crown visible", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 1); + AddUntilStep("second user crown hidden", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 0); + + AddStep("make second user host", () => Client.TransferHost(3)); + + AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); + AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs new file mode 100644 index 0000000000..306a54bfdc --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ParticipantPanel : RealtimeRoomComposite, IHasContextMenu + { + public readonly MultiplayerRoomUser User; + + [Resolved] + private IAPIProvider api { get; set; } + + private ReadyMark readyMark; + private SpriteIcon crown; + + public ParticipantPanel(MultiplayerRoomUser user) + { + User = user; + + RelativeSizeAxes = Axes.X; + Height = 40; + } + + [BackgroundDependencyLoader] + private void load() + { + Debug.Assert(User.User != null); + + var backgroundColour = Color4Extensions.FromHex("#33413C"); + + InternalChildren = new Drawable[] + { + crown = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.Crown, + Size = new Vector2(14), + Colour = Color4Extensions.FromHex("#F7E65D"), + Alpha = 0 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 24 }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new UserCoverBackground + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Width = 0.75f, + User = User.User, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f)) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(10), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new UpdateableAvatar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + User = User.User + }, + new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 20), + Country = User.User.Country + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), + Text = User.User.Username + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14), + Text = User.User.CurrentModeRank != null ? $"#{User.User.CurrentModeRank}" : string.Empty + } + } + }, + readyMark = new ReadyMark + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, + Alpha = 0 + } + } + } + } + }; + } + + protected override void OnRoomChanged() + { + base.OnRoomChanged(); + + if (Room == null) + return; + + if (User.State == MultiplayerUserState.Ready) + readyMark.FadeIn(50); + else + readyMark.FadeOut(50); + + if (Room.Host?.Equals(User) == true) + crown.FadeIn(50); + else + crown.FadeOut(50); + } + + public MenuItem[] ContextMenuItems + { + get + { + if (Room == null) + return null; + + // If the local user is targetted. + if (User.UserID == api.LocalUser.Value.Id) + return null; + + // If the local user is not the host of the room. + if (Room.Host?.UserID != api.LocalUser.Value.Id) + return null; + + int targetUser = User.UserID; + + return new MenuItem[] + { + new OsuMenuItem("Give host", MenuItemType.Standard, () => + { + // Ensure the local user is still host. + if (Room.Host?.UserID != api.LocalUser.Value.Id) + return; + + Client.TransferHost(targetUser); + }) + }; + } + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs new file mode 100644 index 0000000000..d4c32d9189 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ParticipantsList : RealtimeRoomComposite + { + private FillFlowContainer panels; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = panels = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2) + } + } + }; + } + + protected override void OnRoomChanged() + { + base.OnRoomChanged(); + + if (Room == null) + panels.Clear(); + else + { + // Remove panels for users no longer in the room. + panels.RemoveAll(p => !Room.Users.Contains(p.User)); + + // Add panels for all users new to the room. + foreach (var user in Room.Users.Except(panels.Select(p => p.User))) + panels.Add(new ParticipantPanel(user)); + } + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs new file mode 100644 index 0000000000..df49d9342e --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ReadyMark : CompositeDrawable + { + public ReadyMark() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), + Text = "ready", + Colour = Color4Extensions.FromHex("#DDFFFF") + }, + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.CheckCircle, + Size = new Vector2(12), + Colour = Color4Extensions.FromHex("#AADD00") + } + } + }; + } + } +} From 11a903a206a820191af25de9e4f953223869c44a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:46:16 +0900 Subject: [PATCH 3/4] Add test for many users and disable scrollbar --- .../TestSceneParticipantsList.cs | 20 +++++++++++++++++++ .../Participants/ParticipantsList.cs | 1 + 2 files changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs index ee6bbc4ecd..8c997e9e32 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs @@ -92,5 +92,25 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); } + + [Test] + public void TestManyUsers() + { + AddStep("add many users", () => + { + for (int i = 0; i < 20; i++) + { + Client.AddUser(new User + { + Id = i, + Username = $"User {i}", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + if (i % 2 == 0) + Client.ChangeUserState(i, MultiplayerUserState.Ready); + } + }); + } } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs index d4c32d9189..218c2cabb7 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants Child = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, Child = panels = new FillFlowContainer { RelativeSizeAxes = Axes.X, From ce2560b545deea1076c5f6cfd781e9089a360061 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:36:31 +0900 Subject: [PATCH 4/4] Extract value into const --- .../Participants/ParticipantPanel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs index 306a54bfdc..002849a275 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -142,15 +142,17 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants if (Room == null) return; + const double fade_time = 50; + if (User.State == MultiplayerUserState.Ready) - readyMark.FadeIn(50); + readyMark.FadeIn(fade_time); else - readyMark.FadeOut(50); + readyMark.FadeOut(fade_time); if (Room.Host?.Equals(User) == true) - crown.FadeIn(50); + crown.FadeIn(fade_time); else - crown.FadeOut(50); + crown.FadeOut(fade_time); } public MenuItem[] ContextMenuItems