diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 158a1f46a0..50c4c3439b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using NUnit.Framework; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -15,6 +16,7 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Mods; @@ -24,6 +26,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -41,7 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestAddUser() { - AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); + AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 1); AddStep("add user", () => MultiplayerClient.AddUser(new APIUser { @@ -50,20 +53,20 @@ namespace osu.Game.Tests.Visual.Multiplayer CoverUrl = TestResources.COVER_IMAGE_3, })); - AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); + AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 2); } [Test] public void TestAddUnresolvedUser() { - AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); + AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 1); AddStep("add non-resolvable user", () => MultiplayerClient.TestAddUnresolvedUser()); AddUntilStep("null user added", () => MultiplayerClient.ClientRoom.AsNonNull().Users.Count(u => u.User == null) == 1); - AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); + AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 2); - AddStep("kick null user", () => this.ChildrenOfType().Single(p => p.User.User == null) + AddStep("kick null user", () => this.ChildrenOfType().Single(p => p.Current.Value.User == null) .ChildrenOfType().Single().TriggerClick()); AddUntilStep("null user kicked", () => MultiplayerClient.ClientRoom.AsNonNull().Users.Count == 1); @@ -86,7 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value)); - AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.UserID == secondUser?.Id); + AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().Current.Value.UserID == secondUser?.Id); } [Test] @@ -122,7 +125,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddRepeatStep("increment progress", () => { - float progress = this.ChildrenOfType().Single().User.BeatmapAvailability.DownloadProgress ?? 0; + float progress = this.ChildrenOfType().Single().Current.Value.BeatmapAvailability.DownloadProgress ?? 0; MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f))); }, 25); @@ -163,13 +166,17 @@ namespace osu.Game.Tests.Visual.Multiplayer CoverUrl = TestResources.COVER_IMAGE_3, })); - 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); + AddUntilStep("first user crown visible", + () => this.ChildrenOfType().Single(p => p.Current.Value.UserID == 1001).ChildrenOfType().First().Alpha == 1); + AddUntilStep("second user crown hidden", + () => this.ChildrenOfType().Single(p => p.Current.Value.UserID == 3).ChildrenOfType().First().Alpha == 0); AddStep("make second user host", () => MultiplayerClient.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); + AddUntilStep("first user crown visible", + () => this.ChildrenOfType().Single(p => p.Current.Value.UserID == 1001).ChildrenOfType().First().Alpha == 0); + AddUntilStep("second user crown hidden", + () => this.ChildrenOfType().Single(p => p.Current.Value.UserID == 3).ChildrenOfType().First().Alpha == 1); } [Test] @@ -185,9 +192,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); AddAssert("second user above first", () => { - var first = this.ChildrenOfType().ElementAt(0); - var second = this.ChildrenOfType().ElementAt(1); - return second.Y < first.Y; + var first = this.ChildrenOfType().Single(u => u.Current.Value.UserID == 1001); + var second = this.ChildrenOfType().Single(u => u.Current.Value.UserID == 3); + return second.ScreenSpaceDrawQuad.TopLeft.Y < first.ScreenSpaceDrawQuad.TopLeft.Y; }); } @@ -230,7 +237,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestManyUsers() { - const int users_count = 20; + const int users_count = 200; AddStep("add many users", () => { @@ -276,6 +283,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddRepeatStep("switch hosts", () => MultiplayerClient.TransferHost(RNG.Next(0, users_count)), 10); AddStep("give host back", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id)); + + AddRepeatStep("perform many random state changes at once", () => + { + for (int i = 0; i < users_count; ++i) + { + MultiplayerClient.ChangeUserBeatmapAvailability(i, BeatmapAvailability.LocallyAvailable()); + MultiplayerClient.ChangeUserState(i, RNG.NextBool() ? MultiplayerUserState.Idle : MultiplayerUserState.Ready); + } + }, 100); } [Test] @@ -434,6 +450,35 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + [Test] + public void TestTeams() + { + AddStep("enable teams", () => MultiplayerClient.ChangeSettings(matchType: MatchType.TeamVersus)); + AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 1); + + int id = 3; + AddRepeatStep("add users", () => MultiplayerClient.AddUser(new APIUser + { + Id = Interlocked.Increment(ref id), + Username = "Second", + CoverUrl = TestResources.COVER_IMAGE_3, + }), 5); + + AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.Current.Value).Distinct().Count() == 6); + + AddAssert("user 1001 on red team", + () => (MultiplayerClient.ClientRoom!.Users.Single(u => u.UserID == 1001).MatchState as TeamVersusUserState)?.TeamID, + () => Is.EqualTo(0)); + AddStep("click first team indicator", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("user 1001 on blue team", + () => (MultiplayerClient.ClientRoom!.Users.Single(u => u.UserID == 1001).MatchState as TeamVersusUserState)?.TeamID, + () => Is.EqualTo(1)); + } + private void createNewParticipantsList() { ParticipantsList? participantsList = null; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 0cedfb9909..e55f8de61b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -7,12 +7,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; @@ -36,9 +37,17 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { - public partial class ParticipantPanel : CompositeDrawable, IHasContextMenu + public partial class ParticipantPanel : PoolableDrawable, IHasContextMenu, IHasCurrentValue { - public readonly MultiplayerRoomUser User; + public const int HEIGHT = 40; + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(new MultiplayerRoomUser(-1)); [Resolved] private IAPIProvider api { get; set; } = null!; @@ -51,6 +60,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private SpriteIcon crown = null!; + private UserCoverBackground userCover = null!; + private UpdateableAvatar userAvatar = null!; + private OsuSpriteText username = null!; + private Container teamFlagContainer = null!; private OsuSpriteText userRankText = null!; private StyleDisplayIcon userStyleDisplay = null!; private ModDisplay userModsDisplay = null!; @@ -58,19 +71,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private IconButton kickButton = null!; - public ParticipantPanel(MultiplayerRoomUser user) + public ParticipantPanel() { - User = user; - RelativeSizeAxes = Axes.X; - Height = 40; + Height = HEIGHT; } [BackgroundDependencyLoader] private void load() { - var user = User.User; - var backgroundColour = Color4Extensions.FromHex("#33413C"); InternalChild = new GridContainer @@ -96,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Colour = Color4Extensions.FromHex("#F7E65D"), Alpha = 0 }, - new TeamDisplay(User), + new TeamDisplay { Current = Current }, new Container { RelativeSizeAxes = Axes.Both, @@ -109,13 +118,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants RelativeSizeAxes = Axes.Both, Colour = backgroundColour }, - new UserCoverBackground + userCover = new UserCoverBackground { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, Width = 0.75f, - User = user, Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f)) }, new FillFlowContainer @@ -125,33 +133,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Direction = FillDirection.Horizontal, Children = new Drawable[] { - new UpdateableAvatar + userAvatar = new UpdateableAvatar { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, - User = user }, new UpdateableFlag { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(28, 20), - CountryCode = user?.CountryCode ?? default }, - new UpdateableTeamFlag(user?.Team) + teamFlagContainer = new Container { + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(40, 20), }, - new OsuSpriteText + username = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), - Text = user?.Username ?? string.Empty }, userRankText = new OsuSpriteText { @@ -198,18 +203,50 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.Centre, Alpha = 0, Margin = new MarginPadding(4), - Action = () => client.KickUser(User.UserID).FireAndForget(), + Action = () => client.KickUser(current.Value.UserID).FireAndForget(), }, }, } }; } - protected override void LoadComplete() + protected override void PrepareForUse() { - base.LoadComplete(); + base.PrepareForUse(); client.RoomUpdated += onRoomUpdated; + updateUser(); + FinishTransforms(true); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + client.RoomUpdated -= onRoomUpdated; + // this is a safety measure. + // `MultiplayerRoomUser` has equality members overridden to compare by `UserID` only. + // `MultiplayerClient` only delivers updates of fields values to specific object references. + // if this operation is not done here, in a scenario wherein a user quits and rejoins a room, + // it is possible for a single poolable panel to be freed and then used for the same user with the same ID, + // which at bindable level will lead to `current` not changing (because of the overridden equality member), + // which will lead to this instance not showing any updates for the user in question + // because it's associated with an object reference that `MultiplayerClient` is no longer updating. + current.SetDefault(); + } + + private void updateUser() + { + var user = current.Value.User; + + userCover.User = user; + userAvatar.User = user; + teamFlagContainer.Child = new UpdateableTeamFlag(user?.Team) + { + Size = new Vector2(40, 20) + }; + username.Text = user?.Username ?? string.Empty; + updateState(); } @@ -222,13 +259,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; + var user = current.Value; + if (client.Room.GetCurrentItem() is MultiplayerPlaylistItem currentItem) { - int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID; - int userRulesetId = User.RulesetId ?? currentItem.RulesetID; + int userBeatmapId = user.BeatmapId ?? currentItem.BeatmapID; + int userRulesetId = user.RulesetId ?? currentItem.RulesetID; Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance(); - int? currentModeRank = userRuleset == null ? null : User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; + int? currentModeRank = userRuleset == null ? null : user.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID) @@ -238,12 +277,12 @@ 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 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty() : User.Mods.Select(m => m.ToMod(userRuleset)).ToList()); + Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty() : user.Mods.Select(m => m.ToMod(userRuleset)).ToList()); } - userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); + userStateDisplay.UpdateStatus(user.State, user.BeatmapAvailability); - if (User.BeatmapAvailability.State == DownloadState.LocallyAvailable && User.State != MultiplayerUserState.Spectating) + if (user.BeatmapAvailability.State == DownloadState.LocallyAvailable && user.State != MultiplayerUserState.Spectating) { userModsDisplay.FadeIn(fade_time); userStyleDisplay.FadeIn(fade_time); @@ -254,8 +293,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStyleDisplay.FadeOut(fade_time); } - kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0; - crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0; + kickButton.Alpha = client.IsHost && !user.Equals(client.LocalUser) ? 1 : 0; + crown.Alpha = client.Room.Host?.Equals(user) == true ? 1 : 0; } public MenuItem[]? ContextMenuItems @@ -265,15 +304,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants if (client.Room == null) return null; + var user = current.Value; + // If the local user is targetted. - if (User.UserID == api.LocalUser.Value.Id) + if (user.UserID == api.LocalUser.Value.Id) return null; // If the local user is not the host of the room. if (client.Room.Host?.UserID != api.LocalUser.Value.Id) return null; - int targetUser = User.UserID; + int targetUser = user.UserID; return new MenuItem[] { @@ -297,14 +338,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (client.IsNotNull()) - client.RoomUpdated -= onRoomUpdated; - } - public partial class KickButton : IconButton { public KickButton() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index a9d7f4ab52..b553fcc9cd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -3,40 +3,34 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osu.Game.Online.Multiplayer; -using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { - public partial class ParticipantsList : CompositeDrawable + public partial class ParticipantsList : VirtualisedListContainer { - private FillFlowContainer panels = null!; - private ParticipantPanel? currentHostPanel; + private BindableList participants => RowData; + + private MultiplayerRoomUser? currentHost; [Resolved] private MultiplayerClient client { get; set; } = null!; - [BackgroundDependencyLoader] - private void load() + public ParticipantsList() + : base(ParticipantPanel.HEIGHT, initialPoolSize: 20) { - InternalChild = new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = panels = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 2) - } - }; } + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer + { + ScrollbarVisible = false, + }; + protected override void LoadComplete() { base.LoadComplete(); @@ -50,36 +44,38 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private void updateState() { if (client.Room == null) - panels.Clear(); + participants.Clear(); else { // Remove panels for users no longer in the room. - foreach (var p in panels) + for (int i = participants.Count - 1; i >= 0; i--) { + var participant = participants[i]; + // Note that we *must* use reference equality here, as this call is scheduled and a user may have left and joined since it was last run. - if (client.Room.Users.All(u => !ReferenceEquals(p.User, u))) - p.Expire(); + if (client.Room.Users.All(u => !ReferenceEquals(participant, u))) + participants.RemoveAt(i); } // Add panels for all users new to the room. - foreach (var user in client.Room.Users.Except(panels.Select(p => p.User))) - panels.Add(new ParticipantPanel(user)); + foreach (var user in client.Room.Users.Except(participants)) + participants.Add(user); - if (currentHostPanel == null || !currentHostPanel.User.Equals(client.Room.Host)) + if (currentHost == null || !currentHost.Equals(client.Room.Host)) { - // Reset position of previous host back to normal, if one existing. - if (currentHostPanel != null && panels.Contains(currentHostPanel)) - panels.SetLayoutPosition(currentHostPanel, 0); - - currentHostPanel = null; + currentHost = null; // Change position of new host to display above all participants. if (client.Room.Host != null) { - currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(client.Room.Host)); + currentHost = participants.SingleOrDefault(u => u.Equals(client.Room.Host)); + int currentHostIndex = participants.IndexOf(client.Room.Host); - if (currentHostPanel != null) - panels.SetLayoutPosition(currentHostPanel, -1); + if (currentHostIndex > 0) + { + participants.Move(currentHostIndex, 0); + currentHost = participants[0]; + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs index bd9511d50d..282430d744 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs @@ -5,11 +5,13 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.Multiplayer; @@ -19,9 +21,15 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { - internal partial class TeamDisplay : CompositeDrawable + internal partial class TeamDisplay : CompositeDrawable, IHasCurrentValue { - private readonly MultiplayerRoomUser user; + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(new MultiplayerRoomUser(-1)); [Resolved] private OsuColour colours { get; set; } = null!; @@ -33,10 +41,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private Drawable box = null!; private Sample? sampleTeamSwap; - public TeamDisplay(MultiplayerRoomUser user) + public TeamDisplay() { - this.user = user; - RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; Margin = new MarginPadding { Horizontal = 3 }; @@ -69,12 +75,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } }; - if (client.LocalUser?.Equals(user) == true) - { - clickableContent.Action = changeTeam; - clickableContent.TooltipText = "Change team"; - } - sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap"); } @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants base.LoadComplete(); client.RoomUpdated += onRoomUpdated; - updateState(); + current.BindValueChanged(_ => updateUser(), true); } private void changeTeam() @@ -96,12 +96,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants public int? DisplayedTeam { get; private set; } + private void updateUser() + { + var user = current.Value; + + if (client.LocalUser?.Equals(user) == true) + { + clickableContent.Action = changeTeam; + clickableContent.TooltipText = "Change team"; + } + + // reset to ensure samples don't play + DisplayedTeam = null; + updateState(); + } + private void onRoomUpdated() => Scheduler.AddOnce(updateState); private void updateState() { // we don't have a way of knowing when an individual user's state has updated, so just handle on RoomUpdated for now. + var user = current.Value; var userRoomState = client.Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState; const double duration = 400;