diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 4fb9d724b5..7d6c76bc2f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -36,18 +36,23 @@ namespace osu.Game.Online.Multiplayer [Key(5)] public IEnumerable AllowedMods { get; set; } = Enumerable.Empty(); + [Key(6)] + public long PlaylistItemId { get; set; } + public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && BeatmapChecksum == other.BeatmapChecksum && RequiredMods.SequenceEqual(other.RequiredMods) && AllowedMods.SequenceEqual(other.AllowedMods) && RulesetID == other.RulesetID - && Name.Equals(other.Name, StringComparison.Ordinal); + && Name.Equals(other.Name, StringComparison.Ordinal) + && PlaylistItemId == other.PlaylistItemId; public override string ToString() => $"Name:{Name}" + $" Beatmap:{BeatmapID} ({BeatmapChecksum})" + $" RequiredMods:{string.Join(',', RequiredMods)}" + $" AllowedMods:{string.Join(',', AllowedMods)}" - + $" Ruleset:{RulesetID}"; + + $" Ruleset:{RulesetID}" + + $" Item:{PlaylistItemId}"; } } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 639dce9230..bfd505fb19 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -66,6 +66,8 @@ namespace osu.Game.Online.Multiplayer /// public readonly BindableList CurrentMatchPlayingUserIds = new BindableList(); + public readonly Bindable CurrentMatchPlayingItem = new Bindable(); + /// /// The corresponding to the local player, if available. /// @@ -92,10 +94,11 @@ namespace osu.Game.Online.Multiplayer [Resolved] private RulesetStore rulesets { get; set; } = null!; - private Room? apiRoom; + // Only exists for compatibility with old osu-server-spectator build. + // Todo: Can be removed on 2021/02/26. + private long defaultPlaylistItemId; - // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. - private long playlistItemId; + private Room? apiRoom; [BackgroundDependencyLoader] private void load() @@ -142,7 +145,7 @@ namespace osu.Game.Online.Multiplayer { Room = joinedRoom; apiRoom = room; - playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0; }, cancellationSource.Token); // Update room settings. @@ -218,7 +221,7 @@ namespace osu.Game.Online.Multiplayer BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods, - AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods + AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods, }); } @@ -506,14 +509,13 @@ namespace osu.Game.Online.Multiplayer Room.Settings = settings; apiRoom.Name.Value = Room.Settings.Name; - // The playlist update is delayed until an online beatmap lookup (below) succeeds. - // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. - apiRoom.Playlist.Clear(); + // The current item update is delayed until an online beatmap lookup (below) succeeds. + // In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here. + CurrentMatchPlayingItem.Value = null; RoomUpdated?.Invoke(); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => { if (cancellationToken.IsCancellationRequested) @@ -540,18 +542,30 @@ namespace osu.Game.Online.Multiplayer var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset)); var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); - PlaylistItem playlistItem = new PlaylistItem + // Try to retrieve the existing playlist item from the API room. + var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId); + + if (playlistItem != null) + updateItem(playlistItem); + else { - ID = playlistItemId, - Beatmap = { Value = beatmap }, - Ruleset = { Value = ruleset.RulesetInfo }, - }; + // An existing playlist item does not exist, so append a new one. + updateItem(playlistItem = new PlaylistItem()); + apiRoom.Playlist.Add(playlistItem); + } - playlistItem.RequiredMods.AddRange(mods); - playlistItem.AllowedMods.AddRange(allowedMods); + CurrentMatchPlayingItem.Value = playlistItem; - apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. - apiRoom.Playlist.Add(playlistItem); + void updateItem(PlaylistItem item) + { + item.ID = settings.PlaylistItemId == 0 ? defaultPlaylistItemId : settings.PlaylistItemId; + item.Beatmap.Value = beatmap; + item.Ruleset.Value = ruleset.RulesetInfo; + item.RequiredMods.Clear(); + item.RequiredMods.AddRange(mods); + item.AllowedMods.Clear(); + item.AllowedMods.AddRange(allowedMods); + } } /// diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 61982101c1..1d409d4b56 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -23,6 +23,12 @@ namespace osu.Game.Online.Rooms [JsonProperty("ruleset_id")] public int RulesetID { get; set; } + /// + /// Whether this is still a valid selection for the . + /// + [JsonProperty("expired")] + public bool Expired { get; set; } + [JsonIgnore] public readonly Bindable Beatmap = new Bindable(); diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 1774eb4aca..b28680ffef 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -153,6 +153,12 @@ namespace osu.Game.Online.Rooms if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); + // Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended, + // and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room. + // More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room. + if (!(Status.Value is RoomStatusEnded)) + other.Playlist.RemoveAll(i => i.Expired); + if (!Playlist.SequenceEqual(other.Playlist)) { Playlist.Clear(); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index da516798c8..52705302aa 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -25,6 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Match [Cached(typeof(IPreviewTrackOwner))] public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner { + [Cached(typeof(IBindable))] protected readonly Bindable SelectedItem = new Bindable(); public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -39,9 +39,6 @@ namespace osu.Game.Screens.OnlinePlay.Match private Sample sampleStart; - [Resolved(typeof(Room), nameof(Room.Playlist))] - protected BindableList Playlist { get; private set; } - /// /// Any mods applied by/to the local user. /// @@ -103,8 +100,6 @@ namespace osu.Game.Screens.OnlinePlay.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - Playlist.BindCollectionChanged(onPlaylistChanged, true); - managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); @@ -122,9 +117,6 @@ namespace osu.Game.Screens.OnlinePlay.Match return base.OnBackButton(); } - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => - SelectedItem.Value = Playlist.FirstOrDefault(); - protected void ShowUserModSelect() => userModsSelectOverlay.Show(); public override void OnEntering(IScreen last) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index f17e04d4d4..ebe63e26d6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Specialized; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,7 +11,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class BeatmapSelectionControl : OnlinePlayComposite + public class BeatmapSelectionControl : RoomSubScreenComposite { [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } @@ -60,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - Playlist.BindCollectionChanged(onPlaylistChanged, true); + SelectedItem.BindValueChanged(_ => updateBeatmap(), true); Host.BindValueChanged(host => { if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true) @@ -70,12 +68,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }, true); } - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + private void updateBeatmap() { - if (Playlist.Any()) - beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(Playlist.Single(), false, false); - else + if (SelectedItem.Value == null) beatmapPanelContainer.Clear(); + else + beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(SelectedItem.Value, false, false); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ca48139287..4fbea4e3be 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerMatchSettingsOverlay settingsOverlay; - private IBindable isConnected; + private readonly IBindable isConnected = new Bindable(); [CanBeNull] private IDisposable readyClickOperation; @@ -252,13 +252,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); + SelectedItem.BindTo(client.CurrentMatchPlayingItem); + BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; - isConnected = client.IsConnected.GetBoundCopy(); + isConnected.BindTo(client.IsConnected); isConnected.BindValueChanged(connected => { if (!connected.NewValue) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index edae4aeec5..eb0b23f13f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -2,14 +2,19 @@ // 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.Graphics.Containers; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay { + /// + /// A that exposes bindables for properties. + /// public class OnlinePlayComposite : CompositeDrawable { [Resolved(typeof(Room))] @@ -53,5 +58,23 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Duration { get; private set; } + + /// + /// The currently selected item in the , or the first item from + /// if this is not within a . + /// + protected readonly Bindable SelectedItem = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true); + } + + protected virtual void UpdateSelectedItem() + { + SelectedItem.Value = Playlist.FirstOrDefault(); + } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 3f2873cbc4..f0c77b79bf 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -33,6 +34,10 @@ namespace osu.Game.Screens.OnlinePlay protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); + [CanBeNull] + [Resolved(CanBeNull = true)] + private IBindable selectedItem { get; set; } + private readonly FreeModSelectOverlay freeModSelectOverlay; private WorkingBeatmap initialBeatmap; @@ -67,8 +72,8 @@ namespace osu.Game.Screens.OnlinePlay // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. // Similarly, freeMods is currently empty but should only contain the allowed mods. - Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); - FreeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); Ruleset.BindValueChanged(onRulesetChanged); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 694d804f6d..6542d01e64 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -29,6 +29,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved(typeof(Room), nameof(Room.RoomID))] private Bindable roomId { get; set; } + [Resolved(typeof(Room), nameof(Room.Playlist))] + private BindableList playlist { get; set; } + private MatchSettingsOverlay settingsOverlay; private MatchLeaderboard leaderboard; @@ -119,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new DrawableRoomPlaylistWithResults { RelativeSizeAxes = Axes.Both, - Items = { BindTarget = Playlist }, + Items = { BindTarget = playlist }, SelectedItem = { BindTarget = SelectedItem }, RequestShowResults = item => { @@ -266,7 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // Set the first playlist item. // This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). - Schedule(() => SelectedItem.Value = Playlist.FirstOrDefault()); + Schedule(() => SelectedItem.Value = playlist.FirstOrDefault()); } }, true); } diff --git a/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs b/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs new file mode 100644 index 0000000000..4cfd881aa3 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Match; + +namespace osu.Game.Screens.OnlinePlay +{ + /// + /// An with additional logic tracking the currently-selected inside a . + /// + public class RoomSubScreenComposite : OnlinePlayComposite + { + [Resolved] + private IBindable subScreenSelectedItem { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + subScreenSelectedItem.BindValueChanged(_ => UpdateSelectedItem(), true); + } + + protected override void UpdateSelectedItem() + { + if (RoomID.Value == null) + { + // If the room hasn't been created yet, fall-back to the base logic. + base.UpdateSelectedItem(); + return; + } + + SelectedItem.Value = subScreenSelectedItem.Value; + } + } +}