Merge pull request #11808 from smoogipoo/multiplayer-no-playlist-mangling

Rework multiplayer playlist handling to support multiple items
This commit is contained in:
Dean Herbert 2021-02-19 14:33:53 +09:00 committed by GitHub
commit a2aec6bcdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 154 additions and 57 deletions

View File

@ -36,18 +36,23 @@ namespace osu.Game.Online.Multiplayer
[Key(5)]
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();
[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}";
}
}

View File

@ -66,6 +66,8 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
public readonly BindableList<int> CurrentMatchPlayingUserIds = new BindableList<int>();
public readonly Bindable<PlaylistItem?> CurrentMatchPlayingItem = new Bindable<PlaylistItem?>();
/// <summary>
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
/// </summary>
@ -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,20 +542,32 @@ 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 },
};
playlistItem.RequiredMods.AddRange(mods);
playlistItem.AllowedMods.AddRange(allowedMods);
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity.
// An existing playlist item does not exist, so append a new one.
updateItem(playlistItem = new PlaylistItem());
apiRoom.Playlist.Add(playlistItem);
}
CurrentMatchPlayingItem.Value = 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);
}
}
/// <summary>
/// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>.
/// </summary>

View File

@ -23,6 +23,12 @@ namespace osu.Game.Online.Rooms
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
/// <summary>
/// Whether this <see cref="PlaylistItem"/> is still a valid selection for the <see cref="Room"/>.
/// </summary>
[JsonProperty("expired")]
public bool Expired { get; set; }
[JsonIgnore]
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();

View File

@ -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();

View File

@ -21,15 +21,13 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Cached(typeof(IPreviewTrackOwner))]
public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner
{
[Cached(typeof(IBindable<PlaylistItem>))]
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
public override bool DisallowExternalBeatmapRulesetChanges => true;
private Sample sampleStart;
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
/// <summary>
/// Any mods applied by/to the local user.
/// </summary>
@ -73,7 +71,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
base.LoadComplete();
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
SelectedItem.Value = Playlist.FirstOrDefault();
managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
@ -47,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private MultiplayerMatchSettingsOverlay settingsOverlay;
private Drawable userModsSection;
private IBindable<bool> isConnected;
private readonly IBindable<bool> isConnected = new Bindable<bool>();
[CanBeNull]
private IDisposable readyClickOperation;
@ -269,14 +268,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true);
SelectedItem.BindTo(client.CurrentMatchPlayingItem);
SelectedItem.BindValueChanged(onSelectedItemChanged, true);
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)
@ -284,6 +285,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}, true);
}
private void onSelectedItemChanged(ValueChangedEvent<PlaylistItem> item)
{
if (client?.LocalUser == null)
return;
if (item.NewValue?.AllowedMods.Any() != true)
{
userModsSection.Hide();
userModsSelectOverlay.Hide();
userModsSelectOverlay.IsValidMod = _ => false;
}
else
{
userModsSection.Show();
userModsSelectOverlay.IsValidMod = m => item.NewValue.AllowedMods.Any(a => a.GetType() == m.GetType());
}
}
protected override void UpdateMods()
{
if (SelectedItem.Value == null || client.LocalUser == null)
@ -312,23 +331,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return base.OnBackButton();
}
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e)
{
SelectedItem.Value = Playlist.FirstOrDefault();
if (SelectedItem.Value?.AllowedMods.Any() != true)
{
userModsSection.Hide();
userModsSelectOverlay.Hide();
userModsSelectOverlay.IsValidMod = _ => false;
}
else
{
userModsSection.Show();
userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType());
}
}
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedModSettingsUpdate;

View File

@ -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
{
/// <summary>
/// A <see cref="CompositeDrawable"/> that exposes bindables for <see cref="Room"/> properties.
/// </summary>
public class OnlinePlayComposite : CompositeDrawable
{
[Resolved(typeof(Room))]
@ -53,5 +58,23 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected Bindable<TimeSpan?> Duration { get; private set; }
/// <summary>
/// The currently selected item in the <see cref="RoomSubScreen"/>, or the first item from <see cref="Playlist"/>
/// if this <see cref="OnlinePlayComposite"/> is not within a <see cref="RoomSubScreen"/>.
/// </summary>
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
protected override void LoadComplete()
{
base.LoadComplete();
Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true);
}
protected virtual void UpdateSelectedItem()
{
SelectedItem.Value = Playlist.FirstOrDefault();
}
}
}

View File

@ -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;
@ -31,6 +32,10 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[CanBeNull]
[Resolved(CanBeNull = true)]
private IBindable<PlaylistItem> selectedItem { get; set; }
private readonly Bindable<IReadOnlyList<Mod>> freeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private readonly FreeModSelectOverlay freeModSelectOverlay;
@ -66,8 +71,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<Mod>();
freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
freeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
Ruleset.BindValueChanged(onRulesetChanged);
}

View File

@ -27,6 +27,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<long?> roomId { get; set; }
[Resolved(typeof(Room), nameof(Room.Playlist))]
private BindableList<PlaylistItem> playlist { get; set; }
private MatchSettingsOverlay settingsOverlay;
private MatchLeaderboard leaderboard;
@ -117,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new DrawableRoomPlaylistWithResults
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Playlist },
Items = { BindTarget = playlist },
SelectedItem = { BindTarget = SelectedItem },
RequestShowResults = item =>
{
@ -222,7 +225,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);
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// An <see cref="OnlinePlayComposite"/> with additional logic tracking the currently-selected <see cref="PlaylistItem"/> inside a <see cref="RoomSubScreen"/>.
/// </summary>
public class RoomSubScreenComposite : OnlinePlayComposite
{
[Resolved]
private IBindable<PlaylistItem> 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;
}
}
}