From 37dea0ff211db68d591a98a008cc3e4c76879ebb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Apr 2022 17:05:11 +0900 Subject: [PATCH 1/4] Add failing test case --- .../TestSceneMultiplayerPlaylist.cs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index cbd8b472b8..f8c0939ea9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -13,6 +14,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -183,14 +185,41 @@ namespace osu.Game.Tests.Visual.Multiplayer assertItemInHistoryListStep(2, 0); } + [Test] + public void TestInsertedItemDoesNotRefreshAllOthers() + { + AddStep("change to round robin queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin }).WaitSafely()); + + // Add a few items for the local user. + addItemStep(); + addItemStep(); + addItemStep(); + addItemStep(); + addItemStep(); + + DrawableRoomPlaylistItem[] drawableItems = null; + AddStep("get drawable items", () => drawableItems = this.ChildrenOfType().ToArray()); + + // Add 1 item for another user. + AddStep("join second user", () => MultiplayerClient.AddUser(new APIUser { Id = 10 })); + addItemStep(userId: 10); + + // New item inserted towards the top of the list. + assertItemInQueueListStep(7, 1); + AddAssert("all previous playlist items remained", () => drawableItems.All(this.ChildrenOfType().Contains)); + } + /// /// Adds a step to create a new playlist item. /// - private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) + private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () => { - Expired = expired, - PlayedAt = DateTimeOffset.Now - }))); + MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) + { + Expired = expired, + PlayedAt = DateTimeOffset.Now + })).WaitSafely(); + }); /// /// Asserts the position of a given playlist item in the queue list. From 16d4544ff9b4ca72fcd0899c9cd56a1a92f91246 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Apr 2022 17:06:37 +0900 Subject: [PATCH 2/4] Prevent reloads when playlist item order changes --- osu.Game/Online/Rooms/PlaylistItem.cs | 6 ++++-- .../Match/Playlist/MultiplayerPlaylist.cs | 20 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index f696362cbb..a56851cfe6 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Utils; namespace osu.Game.Online.Rooms { @@ -101,13 +102,13 @@ namespace osu.Game.Online.Rooms #endregion - public PlaylistItem With(IBeatmapInfo beatmap) => new PlaylistItem(beatmap) + public PlaylistItem With(Optional beatmap = default, Optional playlistOrder = default) => new PlaylistItem(beatmap.GetOr(Beatmap)) { ID = ID, OwnerID = OwnerID, RulesetID = RulesetID, Expired = Expired, - PlaylistOrder = PlaylistOrder, + PlaylistOrder = playlistOrder.GetOr(PlaylistOrder), PlayedAt = PlayedAt, AllowedMods = AllowedMods, RequiredMods = RequiredMods, @@ -119,6 +120,7 @@ namespace osu.Game.Online.Rooms && Beatmap.OnlineID == other.Beatmap.OnlineID && RulesetID == other.RulesetID && Expired == other.Expired + && PlaylistOrder == other.PlaylistOrder && AllowedMods.SequenceEqual(other.AllowedMods) && RequiredMods.SequenceEqual(other.RequiredMods); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 879a21e7c1..41f548a630 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -117,8 +117,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { base.PlaylistItemChanged(item); - removeItemFromLists(item.ID); - addItemToLists(item); + var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID); + var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID); + + // Test if the only change between the two playlist items is the order. + if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem)) + { + // Set the new playlist order directly without refreshing the DrawablePlaylistItem. + existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder; + + // The following isn't really required, but is here for safety and explicitness. + // MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation. + queueList.Invalidate(); + } + else + { + removeItemFromLists(item.ID); + addItemToLists(item); + } } private void addItemToLists(MultiplayerPlaylistItem item) From 6e6271d0c0ac4e00b0b2fd18b124e3cd4a0a6ca2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Apr 2022 18:31:17 +0900 Subject: [PATCH 3/4] Fix "server-side" room playlist not updated Remove unused using --- .../TestSceneMultiplayerPlaylist.cs | 1 - osu.Game/Online/Rooms/PlaylistItem.cs | 13 ++++++++ .../Multiplayer/TestMultiplayerClient.cs | 33 +++++++++++++++---- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index f8c0939ea9..1231866b36 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index a56851cfe6..6ec884d79c 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -85,6 +85,19 @@ namespace osu.Game.Online.Rooms Beatmap = beatmap; } + public PlaylistItem(MultiplayerPlaylistItem item) + : this(new APIBeatmap { OnlineID = item.BeatmapID }) + { + ID = item.ID; + OwnerID = item.OwnerID; + RulesetID = item.RulesetID; + Expired = item.Expired; + PlaylistOrder = item.PlaylistOrder; + PlayedAt = item.PlayedAt; + RequiredMods = item.RequiredMods.ToArray(); + AllowedMods = item.AllowedMods.ToArray(); + } + public void MarkInvalid() => valid.Value = false; #region Newtonsoft.Json implicit ShouldSerialize() methods diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b9304f713d..0efaf16f99 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -31,7 +31,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public override IBindable IsConnected => isConnected; private readonly Bindable isConnected = new Bindable(true); + /// + /// The local client's . This is not always equivalent to the server-side room. + /// public new Room? APIRoom => base.APIRoom; + public Action? RoomSetupAction; public bool RoomJoined { get; private set; } @@ -46,6 +50,11 @@ namespace osu.Game.Tests.Visual.Multiplayer /// private readonly List serverSidePlaylist = new List(); + /// + /// Guaranteed up-to-date API room. + /// + private Room? serverSideAPIRoom; + private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex]; private int currentIndex; private long lastPlaylistItemId; @@ -192,13 +201,13 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override async Task JoinRoom(long roomId, string? password = null) { - var apiRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId); + serverSideAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId); - if (password != apiRoom.Password.Value) + if (password != serverSideAPIRoom.Password.Value) throw new InvalidOperationException("Invalid password."); serverSidePlaylist.Clear(); - serverSidePlaylist.AddRange(apiRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item))); + serverSidePlaylist.AddRange(serverSideAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item))); lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID); var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id) @@ -210,11 +219,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { Settings = { - Name = apiRoom.Name.Value, - MatchType = apiRoom.Type.Value, + Name = serverSideAPIRoom.Name.Value, + MatchType = serverSideAPIRoom.Type.Value, Password = password, - QueueMode = apiRoom.QueueMode.Value, - AutoStartDuration = apiRoom.AutoStartDuration.Value + QueueMode = serverSideAPIRoom.QueueMode.Value, + AutoStartDuration = serverSideAPIRoom.AutoStartDuration.Value }, Playlist = serverSidePlaylist.ToList(), Users = { localUser }, @@ -479,6 +488,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); Debug.Assert(APIRoom != null); + Debug.Assert(serverSideAPIRoom != null); var item = serverSidePlaylist.Find(i => i.ID == playlistItemId); @@ -495,6 +505,7 @@ namespace osu.Game.Tests.Visual.Multiplayer throw new InvalidOperationException("Attempted to remove an item which has already been played."); serverSidePlaylist.Remove(item); + serverSideAPIRoom.Playlist.RemoveAll(i => i.ID == item.ID); await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false); @@ -576,10 +587,12 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task addItem(MultiplayerPlaylistItem item) { Debug.Assert(Room != null); + Debug.Assert(serverSideAPIRoom != null); item.ID = ++lastPlaylistItemId; serverSidePlaylist.Add(item); + serverSideAPIRoom.Playlist.Add(new PlaylistItem(item)); await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); await updatePlaylistOrder(Room).ConfigureAwait(false); @@ -603,6 +616,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private async Task updatePlaylistOrder(MultiplayerRoom room) { + Debug.Assert(serverSideAPIRoom != null); + List orderedActiveItems; switch (room.Settings.QueueMode) @@ -648,6 +663,10 @@ namespace osu.Game.Tests.Visual.Multiplayer await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); } + + // Also ensure that the API room's playlist is correct. + foreach (var item in serverSideAPIRoom.Playlist) + item.PlaylistOrder = serverSidePlaylist.Single(i => i.ID == item.ID).PlaylistOrder; } } } From e1f147a207a42f47370ac42527226212e23d3f9c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 4 Apr 2022 13:46:41 +0900 Subject: [PATCH 4/4] Mutate playlist in EditUserPlaylistItem --- osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 0efaf16f99..4a974cf61d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -458,8 +458,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item) { Debug.Assert(Room != null); - Debug.Assert(APIRoom != null); Debug.Assert(currentItem != null); + Debug.Assert(serverSideAPIRoom != null); item.OwnerID = userId; @@ -478,6 +478,7 @@ namespace osu.Game.Tests.Visual.Multiplayer item.PlaylistOrder = existingItem.PlaylistOrder; serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item; + serverSideAPIRoom.Playlist[serverSideAPIRoom.Playlist.IndexOf(serverSideAPIRoom.Playlist.Single(i => i.ID == item.ID))] = new PlaylistItem(item); await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); }