1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-12 23:12:59 +08:00

Merge pull request #16020 from smoogipoo/host-enqueue-2

Add support for enqueueing items in host-only mode, and changing arbitrary playlist items
This commit is contained in:
Dean Herbert 2021-12-10 16:23:48 +09:00 committed by GitHub
commit 3633494509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 190 additions and 70 deletions

View File

@ -87,9 +87,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void addItem(Func<BeatmapInfo> beatmap)
{
AddStep("click edit button", () =>
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().AddOrEditPlaylistButton);
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left);
});

View File

@ -255,6 +255,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
p.AllowDeletion = true;
p.AllowShowingResults = true;
p.AllowEditing = true;
});
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osuTK.Input;
@ -74,11 +75,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
}
[Test]
public void TestAddItemsAsHost()
{
addItem(() => OtherBeatmap);
AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2);
}
private void selectNewItem(Func<BeatmapInfo> beatmap)
{
AddStep("click edit button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().AddOrEditPlaylistButton);
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().First());
InputManager.Click(MouseButton.Left);
});
@ -90,5 +99,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
AddUntilStep("selected item is new beatmap", () => Client.CurrentMatchPlayingItem.Value?.Beatmap.Value?.OnlineID == otherBeatmap.OnlineID);
}
private void addItem(Func<BeatmapInfo> beatmap)
{
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
}
}
}

View File

@ -416,8 +416,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap();
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.CurrentMatchPlayingItem.Value);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);

View File

@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new BeatmapCarousel Carousel => base.Carousel;
public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
: base(room, beatmap, ruleset)
: base(room, null, beatmap, ruleset)
{
}
}

View File

@ -83,6 +83,12 @@ namespace osu.Game.Online.Multiplayer
/// <param name="item">The item to add.</param>
Task AddPlaylistItem(MultiplayerPlaylistItem item);
/// <summary>
/// Edits an existing playlist item with new values.
/// </summary>
/// <param name="item">The item to edit, containing new properties. Must have an ID.</param>
Task EditPlaylistItem(MultiplayerPlaylistItem item);
/// <summary>
/// Removes an item from the playlist.
/// </summary>

View File

@ -335,6 +335,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task RemovePlaylistItem(long playlistItemId);
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)

View File

@ -162,6 +162,14 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item);
}
public override Task EditPlaylistItem(MultiplayerPlaylistItem item)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.EditPlaylistItem), item);
}
public override Task RemovePlaylistItem(long playlistItemId)
{
if (!IsConnected.Value)

View File

@ -33,6 +33,11 @@ namespace osu.Game.Screens.OnlinePlay
/// </summary>
public Action<PlaylistItem> RequestResults;
/// <summary>
/// Invoked when an item requests to be edited.
/// </summary>
public Action<PlaylistItem> RequestEdit;
private bool allowReordering;
/// <summary>
@ -104,6 +109,24 @@ namespace osu.Game.Screens.OnlinePlay
}
}
private bool allowEditing;
/// <summary>
/// Whether to allow items to be edited.
/// If <c>true</c>, requests to edit items may be satisfied via <see cref="RequestEdit"/>.
/// </summary>
public bool AllowEditing
{
get => allowEditing;
set
{
allowEditing = value;
foreach (var item in ListContainer.OfType<DrawableRoomPlaylistItem>())
item.AllowEditing = value;
}
}
private bool showItemOwners;
/// <summary>
@ -135,12 +158,14 @@ namespace osu.Game.Screens.OnlinePlay
{
d.SelectedItem.BindTarget = SelectedItem;
d.RequestDeletion = i => RequestDeletion?.Invoke(i);
d.RequestResults = i => RequestResults?.Invoke(i);
d.RequestEdit = i => RequestEdit?.Invoke(i);
d.AllowReordering = AllowReordering;
d.AllowDeletion = AllowDeletion;
d.AllowSelection = AllowSelection;
d.AllowShowingResults = AllowShowingResults;
d.AllowEditing = AllowEditing;
d.ShowItemOwner = ShowItemOwners;
d.RequestResults = i => RequestResults?.Invoke(i);
});
protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item);

View File

@ -51,6 +51,11 @@ namespace osu.Game.Screens.OnlinePlay
/// </summary>
public Action<PlaylistItem> RequestResults;
/// <summary>
/// Invoked when this item requests to be edited.
/// </summary>
public Action<PlaylistItem> RequestEdit;
/// <summary>
/// The currently-selected item, used to show a border around this item.
/// May be updated by this item if <see cref="AllowSelection"/> is <c>true</c>.
@ -74,6 +79,7 @@ namespace osu.Game.Screens.OnlinePlay
private FillFlowContainer buttonsFlow;
private UpdateableAvatar ownerAvatar;
private Drawable showResultsButton;
private Drawable editButton;
private Drawable removeButton;
private PanelBackground panelBackground;
private FillFlowContainer mainFillFlow;
@ -213,6 +219,23 @@ namespace osu.Game.Screens.OnlinePlay
}
}
private bool allowEditing;
/// <summary>
/// Whether this item can be edited.
/// </summary>
public bool AllowEditing
{
get => allowEditing;
set
{
allowEditing = value;
if (editButton != null)
editButton.Alpha = value ? 1 : 0;
}
}
private bool showItemOwner;
/// <summary>
@ -416,6 +439,13 @@ namespace osu.Game.Screens.OnlinePlay
TooltipText = "View results"
},
Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item),
editButton = new PlaylistEditButton
{
Size = new Vector2(30, 30),
Alpha = AllowEditing ? 1 : 0,
Action = () => RequestEdit?.Invoke(Item),
TooltipText = "Edit"
},
removeButton = new PlaylistRemoveButton
{
Size = new Vector2(30, 30),
@ -432,6 +462,14 @@ namespace osu.Game.Screens.OnlinePlay
return true;
}
public class PlaylistEditButton : GrayButton
{
public PlaylistEditButton()
: base(FontAwesome.Solid.Edit)
{
}
}
public class PlaylistRemoveButton : GrayButton
{
public PlaylistRemoveButton()

View File

@ -1,6 +1,7 @@
// 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;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -19,6 +20,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{
public readonly Bindable<MultiplayerPlaylistDisplayMode> DisplayMode = new Bindable<MultiplayerPlaylistDisplayMode>();
/// <summary>
/// Invoked when an item requests to be edited.
/// </summary>
public Action<PlaylistItem> RequestEdit;
private MultiplayerQueueList queueList;
private MultiplayerHistoryList historyList;
private bool firstPopulation = true;
@ -46,7 +52,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
queueList = new MultiplayerQueueList
{
RelativeSizeAxes = Axes.Both,
SelectedItem = { BindTarget = SelectedItem }
SelectedItem = { BindTarget = SelectedItem },
RequestEdit = item => RequestEdit?.Invoke(item)
},
historyList = new MultiplayerHistoryList
{

View File

@ -78,8 +78,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
private void updateDeleteButtonVisibility()
{
AllowDeletion = (Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost)
&& SelectedItem.Value != Item;
bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost;
AllowDeletion = isItemOwner && SelectedItem.Value != Item;
AllowEditing = isItemOwner;
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using osu.Framework.Allocation;
using osu.Framework.Logging;
@ -24,17 +25,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient client { get; set; }
private readonly PlaylistItem itemToEdit;
private LoadingLayer loadingLayer;
/// <summary>
/// Construct a new instance of multiplayer song select.
/// </summary>
/// <param name="room">The room.</param>
/// <param name="itemToEdit">The item to be edited. May be null, in which case a new item will be added to the playlist.</param>
/// <param name="beatmap">An optional initial beatmap selection to perform.</param>
/// <param name="ruleset">An optional initial ruleset selection to perform.</param>
public MultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
public MultiplayerMatchSongSelect(Room room, PlaylistItem itemToEdit = null, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
: base(room)
{
this.itemToEdit = itemToEdit;
if (beatmap != null || ruleset != null)
{
Schedule(() =>
@ -59,14 +65,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
loadingLayer.Show();
client.AddPlaylistItem(new MultiplayerPlaylistItem
var multiplayerItem = new MultiplayerPlaylistItem
{
ID = itemToEdit?.ID ?? 0,
BeatmapID = item.BeatmapID,
BeatmapChecksum = item.Beatmap.Value.MD5Hash,
RulesetID = item.RulesetID,
RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(),
AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray()
}).ContinueWith(t =>
};
Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem);
task.ContinueWith(t =>
{
Schedule(() =>
{

View File

@ -14,7 +14,6 @@ using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@ -44,8 +43,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public override string ShortTitle => "room";
public OsuButton AddOrEditPlaylistButton { get; private set; }
[Resolved]
private MultiplayerClient client { get; set; }
@ -57,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[CanBeNull]
private IDisposable readyClickOperation;
private AddItemButton addItemButton;
public MultiplayerMatchSubScreen(Room room)
: base(room)
{
@ -134,12 +133,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
new Drawable[] { new OverlinedHeader("Beatmap") },
new Drawable[]
{
AddOrEditPlaylistButton = new PurpleTriangleButton
addItemButton = new AddItemButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
Action = SelectBeatmap,
Alpha = 0
Text = "Add item",
Action = () => OpenSongSelection()
},
},
null,
@ -147,7 +146,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
new MultiplayerPlaylist
{
RelativeSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both,
RequestEdit = OpenSongSelection
}
},
new[]
@ -220,12 +220,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
};
internal void SelectBeatmap()
/// <summary>
/// Opens the song selection screen to add or edit an item.
/// </summary>
/// <param name="itemToEdit">An optional playlist item to edit. If null, a new item will be added instead.</param>
internal void OpenSongSelection([CanBeNull] PlaylistItem itemToEdit = null)
{
if (!this.IsCurrentScreen())
return;
this.Push(new MultiplayerMatchSongSelect(Room));
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
}
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
@ -385,23 +389,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
switch (client.Room.Settings.QueueMode)
{
case QueueMode.HostOnly:
AddOrEditPlaylistButton.Text = "Edit beatmap";
AddOrEditPlaylistButton.Alpha = client.IsHost ? 1 : 0;
break;
case QueueMode.AllPlayers:
case QueueMode.AllPlayersRoundRobin:
AddOrEditPlaylistButton.Text = "Add beatmap";
AddOrEditPlaylistButton.Alpha = 1;
break;
default:
AddOrEditPlaylistButton.Alpha = 0;
break;
}
addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0;
Scheduler.AddOnce(UpdateMods);
}
@ -466,7 +454,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
this.Push(new MultiplayerMatchSongSelect(Room, beatmap, ruleset));
this.Push(new MultiplayerMatchSongSelect(Room, SelectedItem.Value, beatmap, ruleset));
}
protected override void Dispose(bool isDisposing)
@ -481,5 +469,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
modSettingChangeTracker?.Dispose();
}
public class AddItemButton : PurpleTriangleButton
{
}
}
}

View File

@ -33,14 +33,14 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[CanBeNull]
[Resolved(CanBeNull = true)]
protected IBindable<PlaylistItem> SelectedItem { get; private set; }
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
[CanBeNull]
[Resolved(CanBeNull = true)]
private IBindable<PlaylistItem> selectedItem { get; set; }
private readonly FreeModSelectOverlay freeModSelectOverlay;
private readonly Room room;
@ -80,8 +80,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 = selectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
Mods.Value = SelectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
FreeMods.Value = SelectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty<Mod>();
Mods.BindValueChanged(onModsChanged);
Ruleset.BindValueChanged(onRulesetChanged);

View File

@ -314,39 +314,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
item.OwnerID = userId;
switch (Room.Settings.QueueMode)
{
case QueueMode.HostOnly:
// In host-only mode, the current item is re-used.
item.ID = currentItem.ID;
item.PlaylistOrder = currentItem.PlaylistOrder;
serverSidePlaylist[currentIndex] = item;
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
// Note: Unlike the server, this is the easiest way to update the current item at this point.
await updateCurrentItem(Room, false).ConfigureAwait(false);
break;
default:
await addItem(item).ConfigureAwait(false);
// The current item can change as a result of an item being added. For example, if all items earlier in the queue were expired.
await updateCurrentItem(Room).ConfigureAwait(false);
break;
}
await addItem(item).ConfigureAwait(false);
await updateCurrentItem(Room).ConfigureAwait(false);
}
public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, item);
public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
{
Debug.Assert(Room != null);
Debug.Assert(APIRoom != null);
Debug.Assert(currentItem != null);
item.OwnerID = userId;
var existingItem = serverSidePlaylist.SingleOrDefault(i => i.ID == item.ID);
if (existingItem == null)
throw new InvalidOperationException("Attempted to change an item that doesn't exist.");
if (existingItem.OwnerID != userId && Room.Host?.UserID != LocalUser?.UserID)
throw new InvalidOperationException("Attempted to change an item which is not owned by the user.");
if (existingItem.Expired)
throw new InvalidOperationException("Attempted to change an item which has already been played.");
// Ensure the playlist order doesn't change.
item.PlaylistOrder = existingItem.PlaylistOrder;
serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item;
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
}
public override Task EditPlaylistItem(MultiplayerPlaylistItem item) => EditUserPlaylistItem(api.LocalUser.Value.OnlineID, item);
public async Task RemoveUserPlaylistItem(int userId, long playlistItemId)
{
Debug.Assert(Room != null);
Debug.Assert(APIRoom != null);
if (Room.Settings.QueueMode == QueueMode.HostOnly)
throw new InvalidOperationException("Items cannot be removed in host-only mode.");
var item = serverSidePlaylist.Find(i => i.ID == playlistItemId);
if (item == null)