1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 10:27:35 +08:00

Replace MultiplayerRoomComposite with local bindings

This commit is contained in:
Dan Balasescu 2024-11-05 19:44:29 +09:00
parent 0811de728e
commit 788ecc1e7b
No known key found for this signature in database
11 changed files with 336 additions and 327 deletions

View File

@ -202,7 +202,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(joinedRoom.Playlist.Count > 0);
APIRoom.Playlist.Clear();
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(item => new PlaylistItem(item)));
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
// The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
@ -734,7 +734,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(APIRoom != null);
Room.Playlist.Add(item);
APIRoom.Playlist.Add(createPlaylistItem(item));
APIRoom.Playlist.Add(new PlaylistItem(item));
ItemAdded?.Invoke(item);
RoomUpdated?.Invoke();
@ -780,7 +780,7 @@ namespace osu.Game.Online.Multiplayer
int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID));
APIRoom.Playlist.RemoveAt(existingIndex);
APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item));
APIRoom.Playlist.Insert(existingIndex, new PlaylistItem(item));
}
catch (Exception ex)
{
@ -853,18 +853,6 @@ namespace osu.Game.Online.Multiplayer
RoomUpdated?.Invoke();
}
private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
{
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()
};
/// <summary>
/// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>.
/// </summary>

View File

@ -0,0 +1,39 @@
// 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.Generic;
using System.Linq;
using osu.Game.Online.Rooms;
namespace osu.Game.Online.Multiplayer
{
public static class MultiplayerRoomExtensions
{
/// <summary>
/// Returns all historical/expired items from the <paramref name="room"/>, in the order in which they were played.
/// </summary>
public static IEnumerable<MultiplayerPlaylistItem> GetHistoricalItems(this MultiplayerRoom room)
=> room.Playlist.Where(item => item.Expired).OrderBy(item => item.PlayedAt);
/// <summary>
/// Returns all non-expired items from the <paramref name="room"/>, in the order in which they are to be played.
/// </summary>
public static IEnumerable<MultiplayerPlaylistItem> GetUpcomingItems(this MultiplayerRoom room)
=> room.Playlist.Where(item => !item.Expired).OrderBy(item => item.PlaylistOrder);
/// <summary>
/// Returns the first non-expired <see cref="MultiplayerPlaylistItem"/> in playlist order from the supplied <paramref name="room"/>,
/// or the last-played <see cref="MultiplayerPlaylistItem"/> if all items are expired,
/// or <see langword="null"/> if <paramref name="room"/> was empty.
/// </summary>
public static MultiplayerPlaylistItem? GetCurrentItem(this MultiplayerRoom room)
{
if (room.Playlist.Count == 0)
return null;
return room.Playlist.All(item => item.Expired)
? GetHistoricalItems(room).Last()
: GetUpcomingItems(room).First();
}
}
}

View File

@ -1,23 +1,25 @@
// 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.
#nullable disable
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public partial class RankRangePill : MultiplayerRoomComposite
public partial class RankRangePill : CompositeDrawable
{
private OsuTextFlowContainer rankFlow;
private OsuTextFlowContainer rankFlow = null!;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
public RankRangePill()
{
@ -55,20 +57,28 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
};
}
protected override void OnRoomUpdated()
protected override void LoadComplete()
{
base.OnRoomUpdated();
base.LoadComplete();
client.RoomUpdated += onRoomUpdated;
updateState();
}
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
private void updateState()
{
rankFlow.Clear();
if (Room == null || Room.Users.All(u => u.User == null))
if (client.Room == null || client.Room.Users.All(u => u.User == null))
{
rankFlow.AddText("-");
return;
}
int minRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min();
int maxRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max();
int minRank = client.Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min();
int maxRank = client.Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max();
rankFlow.AddText("#");
rankFlow.AddText(minRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
@ -78,5 +88,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
rankFlow.AddText("#");
rankFlow.AddText(maxRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
client.RoomUpdated -= onRoomUpdated;
}
}
}

View File

@ -1,47 +1,50 @@
// 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.
#nullable disable
using System;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
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.Containers;
using osu.Framework.Threading;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public partial class MatchStartControl : MultiplayerRoomComposite
public partial class MatchStartControl : CompositeDrawable
{
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[CanBeNull]
private IDisposable clickOperation;
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
private IDialogOverlay? dialogOverlay { get; set; }
private Sample sampleReady;
private Sample sampleReadyAll;
private Sample sampleUnready;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private IBindable<PlaylistItem?> currentItem { get; set; } = null!;
private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerCountdownButton countdownButton;
private IBindable<bool> operationInProgress = null!;
private ScheduledDelegate? readySampleDelegate;
private IDisposable? clickOperation;
private Sample? sampleReady;
private Sample? sampleReadyAll;
private Sample? sampleUnready;
private int countReady;
private ScheduledDelegate readySampleDelegate;
private IBindable<bool> operationInProgress;
public MatchStartControl()
{
@ -91,34 +94,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
base.LoadComplete();
CurrentPlaylistItem.BindValueChanged(_ => updateState());
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
currentItem.BindValueChanged(_ => updateState());
client.RoomUpdated += onRoomUpdated;
client.LoadRequested += onLoadRequested;
updateState();
}
protected override void OnRoomLoadRequested()
{
base.OnRoomLoadRequested();
endOperation();
}
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
private void onLoadRequested() => Scheduler.AddOnce(endOperation);
private void onReadyButtonClick()
{
if (Room == null)
if (client.Room == null)
return;
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
if (Client.IsHost)
if (client.IsHost)
{
if (Room.State == MultiplayerRoomState.Open)
if (client.Room.State == MultiplayerRoomState.Open)
{
if (isReady() && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
if (isReady() && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
startMatch();
else
toggleReady();
@ -131,16 +129,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation));
}
}
else if (Room.State != MultiplayerRoomState.Closed)
else if (client.Room.State != MultiplayerRoomState.Closed)
toggleReady();
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
bool isReady() => client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating;
void toggleReady() => Client.ToggleReady().FireAndForget(
void toggleReady() => client.ToggleReady().FireAndForget(
onSuccess: endOperation,
onError: _ => endOperation());
void startMatch() => Client.StartMatch().FireAndForget(onSuccess: () =>
void startMatch() => client.StartMatch().FireAndForget(onSuccess: () =>
{
// gameplay is starting, the button will be unblocked on load requested.
}, onError: _ =>
@ -149,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
endOperation();
});
void abortMatch() => Client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
void abortMatch() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
}
private void startCountdown(TimeSpan duration)
@ -157,19 +155,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
}
private void cancelCountdown()
{
if (Client.Room == null)
if (client.Room == null)
return;
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
MultiplayerCountdown countdown = Client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
Client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
MultiplayerCountdown countdown = client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
}
private void endOperation()
@ -180,19 +178,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void updateState()
{
if (Room == null)
if (client.Room == null)
{
readyButton.Enabled.Value = false;
countdownButton.Enabled.Value = false;
return;
}
var localUser = Client.LocalUser;
var localUser = client.LocalUser;
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
int newCountReady = client.Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = client.Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
if (!Client.IsHost || Room.Settings.AutoStartEnabled)
if (!client.IsHost || client.Room.Settings.AutoStartEnabled)
countdownButton.Hide();
else
{
@ -211,21 +209,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
readyButton.Enabled.Value = countdownButton.Enabled.Value =
Room.State != MultiplayerRoomState.Closed
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
client.Room.State != MultiplayerRoomState.Closed
&& currentItem.Value?.ID == client.Room.Settings.PlaylistItemId
&& !client.Room.Playlist.Single(i => i.ID == client.Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value;
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating)
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
readyButton.Enabled.Value &= client.IsHost && newCountReady > 0 && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
// When the local user is not the host, the button should only be enabled when no match is in progress.
if (!Client.IsHost)
readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
if (!client.IsHost)
readyButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
// At all times, the countdown button should only be enabled when no match is in progress.
countdownButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
countdownButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
if (newCountReady == countReady)
return;
@ -249,6 +247,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
{
client.RoomUpdated -= onRoomUpdated;
client.LoadRequested -= onLoadRequested;
}
}
public partial class ConfirmAbortDialog : DangerousActionDialog
{
public ConfirmAbortDialog(Action abortMatch, Action cancel)

View File

@ -5,7 +5,9 @@ using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
@ -17,7 +19,7 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public partial class MultiplayerSpectateButton : MultiplayerRoomComposite
public partial class MultiplayerSpectateButton : CompositeDrawable
{
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
@ -25,6 +27,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private IBindable<PlaylistItem?> currentItem { get; set; } = null!;
private IBindable<bool> operationInProgress = null!;
private readonly RoundedButton button;
@ -44,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
var clickOperation = ongoingOperationTracker.BeginOperation();
Client.ToggleSpectate().ContinueWith(_ => endOperation());
client.ToggleSpectate().ContinueWith(_ => endOperation());
void endOperation() => clickOperation?.Dispose();
}
@ -63,19 +71,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
base.LoadComplete();
CurrentPlaylistItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true);
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
currentItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true);
client.RoomUpdated += onRoomUpdated;
updateState();
}
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
private void updateState()
{
switch (Client.LocalUser?.State)
switch (client.LocalUser?.State)
{
default:
button.Text = "Spectate";
@ -88,8 +93,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break;
}
button.Enabled.Value = Client.Room != null
&& Client.Room.State != MultiplayerRoomState.Closed
button.Enabled.Value = client.Room != null
&& client.Room.State != MultiplayerRoomState.Closed
&& !operationInProgress.Value;
Scheduler.AddOnce(checkForAutomaticDownload);
@ -112,11 +117,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void checkForAutomaticDownload()
{
PlaylistItem? currentItem = CurrentPlaylistItem.Value;
PlaylistItem? item = currentItem.Value;
downloadCheckCancellation?.Cancel();
if (currentItem == null)
if (item == null)
return;
if (!automaticallyDownload.Value)
@ -128,13 +133,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
//
// Rather than over-complicating this flow, let's only auto-download when spectating for the time being.
// A potential path forward would be to have a local auto-download checkbox above the playlist item list area.
if (Client.LocalUser?.State != MultiplayerUserState.Spectating)
if (client.LocalUser?.State != MultiplayerUserState.Spectating)
return;
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
beatmapLookupCache
.GetBeatmapAsync(currentItem.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
.GetBeatmapAsync(item.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
.ContinueWith(resolved => Schedule(() =>
{
var beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
@ -150,5 +155,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
#endregion
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
client.RoomUpdated -= onRoomUpdated;
}
}
}

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.
#nullable disable
using System;
using System.Linq;
using osu.Framework.Allocation;
@ -17,18 +15,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
/// <summary>
/// The multiplayer playlist, containing lists to show the items from a <see cref="MultiplayerRoom"/> in both gameplay-order and historical-order.
/// </summary>
public partial class MultiplayerPlaylist : MultiplayerRoomComposite
public partial class MultiplayerPlaylist : CompositeDrawable
{
public readonly Bindable<MultiplayerPlaylistDisplayMode> DisplayMode = new Bindable<MultiplayerPlaylistDisplayMode>();
/// <summary>
/// Invoked when an item requests to be edited.
/// </summary>
public Action<PlaylistItem> RequestEdit;
public Action<PlaylistItem>? RequestEdit;
private MultiplayerPlaylistTabControl playlistTabControl;
private MultiplayerQueueList queueList;
private MultiplayerHistoryList historyList;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private IBindable<PlaylistItem?> currentItem { get; set; } = null!;
private MultiplayerPlaylistTabControl playlistTabControl = null!;
private MultiplayerQueueList queueList = null!;
private MultiplayerHistoryList historyList = null!;
private bool firstPopulation = true;
[BackgroundDependencyLoader]
@ -54,14 +58,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
queueList = new MultiplayerQueueList
{
RelativeSizeAxes = Axes.Both,
SelectedItem = { BindTarget = CurrentPlaylistItem },
SelectedItem = { BindTarget = currentItem },
RequestEdit = item => RequestEdit?.Invoke(item)
},
historyList = new MultiplayerHistoryList
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
SelectedItem = { BindTarget = CurrentPlaylistItem }
SelectedItem = { BindTarget = currentItem }
}
}
}
@ -73,7 +77,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
protected override void LoadComplete()
{
base.LoadComplete();
DisplayMode.BindValueChanged(onDisplayModeChanged, true);
client.ItemAdded += playlistItemAdded;
client.ItemRemoved += playlistItemRemoved;
client.ItemChanged += playlistItemChanged;
client.RoomUpdated += onRoomUpdated;
updateState();
}
private void onDisplayModeChanged(ValueChangedEvent<MultiplayerPlaylistDisplayMode> mode)
@ -82,11 +92,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
queueList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.Queue ? 1 : 0, 100);
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
if (Room == null)
private void updateState()
{
if (client.Room == null)
{
historyList.Items.Clear();
queueList.Items.Clear();
@ -96,34 +106,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
if (firstPopulation)
{
foreach (var item in Room.Playlist)
foreach (var item in client.Room.Playlist)
addItemToLists(item);
firstPopulation = false;
}
}
protected override void PlaylistItemAdded(MultiplayerPlaylistItem item)
{
base.PlaylistItemAdded(item);
addItemToLists(item);
}
private void playlistItemAdded(MultiplayerPlaylistItem item) => Schedule(() => addItemToLists(item));
protected override void PlaylistItemRemoved(long item)
{
base.PlaylistItemRemoved(item);
removeItemFromLists(item);
}
private void playlistItemRemoved(long item) => Schedule(() => removeItemFromLists(item));
protected override void PlaylistItemChanged(MultiplayerPlaylistItem item)
private void playlistItemChanged(MultiplayerPlaylistItem item) => Schedule(() =>
{
base.PlaylistItemChanged(item);
if (client.Room == null)
return;
var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
var newApiItem = new PlaylistItem(item);
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))
if (existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
{
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
@ -137,20 +140,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
removeItemFromLists(item.ID);
addItemToLists(item);
}
}
});
private void addItemToLists(MultiplayerPlaylistItem item)
{
var apiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
var apiItem = client.Room?.Playlist.SingleOrDefault(i => i.ID == item.ID);
// Item could have been removed from the playlist while the local player was in gameplay.
if (apiItem == null)
return;
if (item.Expired)
historyList.Items.Add(apiItem);
historyList.Items.Add(new PlaylistItem(apiItem));
else
queueList.Items.Add(apiItem);
queueList.Items.Add(new PlaylistItem(apiItem));
}
private void removeItemFromLists(long item)

View File

@ -1,125 +0,0 @@
// 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.
#nullable disable
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public abstract partial class MultiplayerRoomComposite : OnlinePlayComposite
{
[CanBeNull]
protected MultiplayerRoom Room => Client.Room;
[Resolved]
protected MultiplayerClient Client { get; private set; }
protected override void LoadComplete()
{
base.LoadComplete();
Client.RoomUpdated += invokeOnRoomUpdated;
Client.LoadRequested += invokeOnRoomLoadRequested;
Client.UserLeft += invokeUserLeft;
Client.UserKicked += invokeUserKicked;
Client.UserJoined += invokeUserJoined;
Client.ItemAdded += invokeItemAdded;
Client.ItemRemoved += invokeItemRemoved;
Client.ItemChanged += invokeItemChanged;
OnRoomUpdated();
}
private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated);
private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => UserJoined(user));
private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.Add(() => UserKicked(user));
private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => UserLeft(user));
private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item));
private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item));
private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item));
private void invokeOnRoomLoadRequested() => Scheduler.AddOnce(OnRoomLoadRequested);
/// <summary>
/// Invoked when a user has joined the room.
/// </summary>
/// <param name="user">The user.</param>
protected virtual void UserJoined(MultiplayerRoomUser user)
{
}
/// <summary>
/// Invoked when a user has been kicked from the room (including the local user).
/// </summary>
/// <param name="user">The user.</param>
protected virtual void UserKicked(MultiplayerRoomUser user)
{
}
/// <summary>
/// Invoked when a user has left the room.
/// </summary>
/// <param name="user">The user.</param>
protected virtual void UserLeft(MultiplayerRoomUser user)
{
}
/// <summary>
/// Invoked when a playlist item is added to the room.
/// </summary>
/// <param name="item">The added playlist item.</param>
protected virtual void PlaylistItemAdded(MultiplayerPlaylistItem item)
{
}
/// <summary>
/// Invoked when a playlist item is removed from the room.
/// </summary>
/// <param name="item">The ID of the removed playlist item.</param>
protected virtual void PlaylistItemRemoved(long item)
{
}
/// <summary>
/// Invoked when a playlist item is changed in the room.
/// </summary>
/// <param name="item">The new playlist item, with an existing item's ID.</param>
protected virtual void PlaylistItemChanged(MultiplayerPlaylistItem item)
{
}
/// <summary>
/// Invoked when any change occurs to the multiplayer room.
/// </summary>
protected virtual void OnRoomUpdated()
{
}
/// <summary>
/// Invoked when the room requests the local user to load into gameplay.
/// </summary>
protected virtual void OnRoomLoadRequested()
{
}
protected override void Dispose(bool isDisposing)
{
if (Client != null)
{
Client.RoomUpdated -= invokeOnRoomUpdated;
Client.LoadRequested -= invokeOnRoomLoadRequested;
Client.UserLeft -= invokeUserLeft;
Client.UserKicked -= invokeUserKicked;
Client.UserJoined -= invokeUserJoined;
Client.ItemAdded -= invokeItemAdded;
Client.ItemRemoved -= invokeItemRemoved;
Client.ItemChanged -= invokeItemChanged;
}
base.Dispose(isDisposing);
}
}
}

View File

@ -1,23 +1,26 @@
// 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.
#nullable disable
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public partial class MultiplayerRoomSounds : MultiplayerRoomComposite
public partial class MultiplayerRoomSounds : CompositeDrawable
{
private Sample hostChangedSample;
private Sample userJoinedSample;
private Sample userLeftSample;
private Sample userKickedSample;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private Sample? hostChangedSample;
private Sample? userJoinedSample;
private Sample? userLeftSample;
private Sample? userKickedSample;
private MultiplayerRoomUser? host;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@ -32,36 +35,47 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
Host.BindValueChanged(hostChanged);
client.RoomUpdated += onRoomUpdated;
client.UserJoined += onUserJoined;
client.UserLeft += onUserLeft;
client.UserKicked += onUserKicked;
updateState();
}
protected override void UserJoined(MultiplayerRoomUser user)
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
private void updateState()
{
base.UserJoined(user);
if (EqualityComparer<MultiplayerRoomUser>.Default.Equals(host, client.Room?.Host))
return;
Scheduler.AddOnce(() => userJoinedSample?.Play());
}
protected override void UserLeft(MultiplayerRoomUser user)
{
base.UserLeft(user);
Scheduler.AddOnce(() => userLeftSample?.Play());
}
protected override void UserKicked(MultiplayerRoomUser user)
{
base.UserKicked(user);
Scheduler.AddOnce(() => userKickedSample?.Play());
}
private void hostChanged(ValueChangedEvent<APIUser> value)
{
// only play sound when the host changes from an already-existing host.
if (value.OldValue == null) return;
if (host != null)
Scheduler.AddOnce(() => hostChangedSample?.Play());
Scheduler.AddOnce(() => hostChangedSample?.Play());
host = client.Room?.Host;
}
private void onUserJoined(MultiplayerRoomUser user)
=> Scheduler.AddOnce(() => userJoinedSample?.Play());
private void onUserLeft(MultiplayerRoomUser user)
=> Scheduler.AddOnce(() => userLeftSample?.Play());
private void onUserKicked(MultiplayerRoomUser user)
=> Scheduler.AddOnce(() => userKickedSample?.Play());
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
{
client.RoomUpdated -= onRoomUpdated;
client.UserJoined -= onUserJoined;
client.UserLeft -= onUserLeft;
client.UserKicked -= onUserKicked;
}
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -30,7 +31,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
public partial class ParticipantPanel : MultiplayerRoomComposite, IHasContextMenu
public partial class ParticipantPanel : CompositeDrawable, IHasContextMenu
{
public readonly MultiplayerRoomUser User;
@ -40,6 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
[Resolved]
private IRulesetStore rulesets { get; set; } = null!;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private SpriteIcon crown = null!;
private OsuSpriteText userRankText = null!;
@ -171,23 +175,31 @@ 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(User.UserID).FireAndForget(),
},
},
}
};
}
protected override void OnRoomUpdated()
protected override void LoadComplete()
{
base.OnRoomUpdated();
base.LoadComplete();
if (Room == null || Client.LocalUser == null)
client.RoomUpdated += onRoomUpdated;
updateState();
}
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
private void updateState()
{
if (client.Room == null || client.LocalUser == null)
return;
const double fade_time = 50;
var currentItem = Playlist.GetCurrentItem();
MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem();
Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
@ -200,8 +212,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
else
userModsDisplay.FadeOut(fade_time);
kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
crown.Alpha = 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;
// 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.
@ -215,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
get
{
if (Room == null)
if (client.Room == null)
return null;
// If the local user is targetted.
@ -223,7 +235,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
return null;
// If the local user is not the host of the room.
if (Room.Host?.UserID != api.LocalUser.Value.Id)
if (client.Room.Host?.UserID != api.LocalUser.Value.Id)
return null;
int targetUser = User.UserID;
@ -233,23 +245,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
{
// Ensure the local user is still host.
if (!Client.IsHost)
if (!client.IsHost)
return;
Client.TransferHost(targetUser).FireAndForget();
client.TransferHost(targetUser).FireAndForget();
}),
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
{
// Ensure the local user is still host.
if (!Client.IsHost)
if (!client.IsHost)
return;
Client.KickUser(targetUser).FireAndForget();
client.KickUser(targetUser).FireAndForget();
})
};
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
client.RoomUpdated -= onRoomUpdated;
}
public partial class KickButton : IconButton
{
public KickButton()

View File

@ -1,24 +1,24 @@
// 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.
#nullable disable
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
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 : MultiplayerRoomComposite
public partial class ParticipantsList : CompositeDrawable
{
private FillFlowContainer<ParticipantPanel> panels;
private FillFlowContainer<ParticipantPanel> panels = null!;
private ParticipantPanel? currentHostPanel;
[CanBeNull]
private ParticipantPanel currentHostPanel;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
@ -37,11 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
};
}
protected override void OnRoomUpdated()
protected override void LoadComplete()
{
base.OnRoomUpdated();
base.LoadComplete();
if (Room == null)
client.RoomUpdated += onRoomUpdated;
updateState();
}
private void onRoomUpdated() => Scheduler.AddOnce(updateState);
private void updateState()
{
if (client.Room == null)
panels.Clear();
else
{
@ -49,15 +57,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
foreach (var p in panels)
{
// 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 (Room.Users.All(u => !ReferenceEquals(p.User, u)))
if (client.Room.Users.All(u => !ReferenceEquals(p.User, u)))
p.Expire();
}
// Add panels for all users new to the room.
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
foreach (var user in client.Room.Users.Except(panels.Select(p => p.User)))
panels.Add(new ParticipantPanel(user));
if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host))
if (currentHostPanel == null || !currentHostPanel.User.Equals(client.Room.Host))
{
// Reset position of previous host back to normal, if one existing.
if (currentHostPanel != null && panels.Contains(currentHostPanel))
@ -66,9 +74,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
currentHostPanel = null;
// Change position of new host to display above all participants.
if (Room.Host != null)
if (client.Room.Host != null)
{
currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host));
currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(client.Room.Host));
if (currentHostPanel != null)
panels.SetLayoutPosition(currentHostPanel, -1);
@ -76,5 +84,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
client.RoomUpdated -= onRoomUpdated;
}
}
}

View File

@ -1,12 +1,11 @@
// 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.
#nullable disable
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -20,27 +19,26 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
internal partial class TeamDisplay : MultiplayerRoomComposite
internal partial class TeamDisplay : CompositeDrawable
{
private readonly MultiplayerRoomUser user;
private Drawable box;
private Sample sampleTeamSwap;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; }
private MultiplayerClient client { get; set; } = null!;
private OsuClickableContainer clickableContent;
private OsuClickableContainer clickableContent = null!;
private Drawable box = null!;
private Sample? sampleTeamSwap;
public TeamDisplay(MultiplayerRoomUser user)
{
this.user = user;
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
Margin = new MarginPadding { Horizontal = 3 };
}
@ -71,7 +69,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}
};
if (Client.LocalUser?.Equals(user) == true)
if (client.LocalUser?.Equals(user) == true)
{
clickableContent.Action = changeTeam;
clickableContent.TooltipText = "Change team";
@ -80,23 +78,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap");
}
protected override void LoadComplete()
{
base.LoadComplete();
client.RoomUpdated += onRoomUpdated;
updateState();
}
private void changeTeam()
{
Client.SendMatchRequest(new ChangeTeamRequest
client.SendMatchRequest(new ChangeTeamRequest
{
TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
TeamID = ((client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
}).FireAndForget();
}
public int? DisplayedTeam { get; private set; }
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
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 userRoomState = Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState;
var userRoomState = client.Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState;
const double duration = 400;
@ -138,5 +144,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
return colours.Blue;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
client.RoomUpdated -= onRoomUpdated;
}
}
}