mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 06:52:56 +08:00
Merge pull request #15890 from smoogipoo/new-multiplayer-playlist
Implement multiplayer playlist with gameplay and historical ordering
This commit is contained in:
commit
b5e5c75a0a
@ -0,0 +1,234 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneMultiplayerPlaylist : MultiplayerTestScene
|
||||
{
|
||||
private MultiplayerPlaylist list;
|
||||
private BeatmapManager beatmaps;
|
||||
private RulesetStore rulesets;
|
||||
private BeatmapSetInfo importedSet;
|
||||
private BeatmapInfo importedBeatmap;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public new void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = list = new MultiplayerPlaylist
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.4f, 0.8f)
|
||||
};
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public new void SetUpSteps()
|
||||
{
|
||||
AddStep("import beatmap", () =>
|
||||
{
|
||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
|
||||
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
|
||||
importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
|
||||
});
|
||||
|
||||
AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonExpiredItemsAddedToQueueList()
|
||||
{
|
||||
assertItemInQueueListStep(1, 0);
|
||||
|
||||
addItemStep();
|
||||
assertItemInQueueListStep(2, 1);
|
||||
|
||||
addItemStep();
|
||||
assertItemInQueueListStep(3, 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExpiredItemsAddedToHistoryList()
|
||||
{
|
||||
assertItemInQueueListStep(1, 0);
|
||||
|
||||
addItemStep(true);
|
||||
assertItemInHistoryListStep(2, 0);
|
||||
|
||||
addItemStep(true);
|
||||
assertItemInHistoryListStep(3, 0);
|
||||
assertItemInHistoryListStep(2, 1);
|
||||
|
||||
// Initial item is still in the queue.
|
||||
assertItemInQueueListStep(1, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExpiredItemsMoveToQueueList()
|
||||
{
|
||||
addItemStep();
|
||||
addItemStep();
|
||||
|
||||
AddStep("finish current item", () => Client.FinishCurrentItem());
|
||||
|
||||
assertItemInHistoryListStep(1, 0);
|
||||
assertItemInQueueListStep(2, 0);
|
||||
assertItemInQueueListStep(3, 1);
|
||||
|
||||
AddStep("finish current item", () => Client.FinishCurrentItem());
|
||||
|
||||
assertItemInHistoryListStep(2, 0);
|
||||
assertItemInHistoryListStep(1, 1);
|
||||
assertItemInQueueListStep(3, 0);
|
||||
|
||||
AddStep("finish current item", () => Client.FinishCurrentItem());
|
||||
|
||||
assertItemInHistoryListStep(3, 0);
|
||||
assertItemInHistoryListStep(2, 1);
|
||||
assertItemInHistoryListStep(1, 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestListsClearedWhenRoomLeft()
|
||||
{
|
||||
addItemStep();
|
||||
AddStep("finish current item", () => Client.FinishCurrentItem());
|
||||
|
||||
AddStep("leave room", () => RoomManager.PartRoom());
|
||||
AddUntilStep("wait for room part", () => Client.Room == null);
|
||||
|
||||
AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0));
|
||||
AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0));
|
||||
}
|
||||
|
||||
[Ignore("Expired items are initially removed from the room.")]
|
||||
[Test]
|
||||
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
|
||||
{
|
||||
AddStep("leave room", () => RoomManager.PartRoom());
|
||||
AddUntilStep("wait for room part", () => Client.Room == null);
|
||||
|
||||
AddStep("join room with items", () =>
|
||||
{
|
||||
RoomManager.CreateRoom(new Room
|
||||
{
|
||||
Name = { Value = "test name" },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
|
||||
Ruleset = { Value = Ruleset.Value }
|
||||
},
|
||||
new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
|
||||
Ruleset = { Value = Ruleset.Value },
|
||||
Expired = true
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
|
||||
assertItemInQueueListStep(1, 0);
|
||||
assertItemInHistoryListStep(2, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a step to create a new playlist item.
|
||||
/// </summary>
|
||||
private void addItemStep(bool expired = false) => AddStep("add item", () => Client.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = importedBeatmap },
|
||||
BeatmapID = importedBeatmap.OnlineID ?? -1,
|
||||
Expired = expired,
|
||||
PlayedAt = DateTimeOffset.Now
|
||||
})));
|
||||
|
||||
/// <summary>
|
||||
/// Asserts the position of a given playlist item in the queue list.
|
||||
/// </summary>
|
||||
/// <param name="playlistItemId">The item id.</param>
|
||||
/// <param name="visualIndex">The index at which the item should appear visually. The item with index 0 is at the top of the list.</param>
|
||||
private void assertItemInQueueListStep(int playlistItemId, int visualIndex)
|
||||
{
|
||||
changeDisplayModeStep(MultiplayerPlaylistDisplayMode.Queue);
|
||||
|
||||
AddUntilStep($"{playlistItemId} in queue at pos = {visualIndex}", () =>
|
||||
{
|
||||
return !inHistoryList(playlistItemId)
|
||||
&& this.ChildrenOfType<MultiplayerQueueList>()
|
||||
.Single()
|
||||
.ChildrenOfType<DrawableRoomPlaylistItem>()
|
||||
.OrderBy(drawable => drawable.Position.Y)
|
||||
.TakeWhile(drawable => drawable.Item.ID != playlistItemId)
|
||||
.Count() == visualIndex;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts the position of a given playlist item in the history list.
|
||||
/// </summary>
|
||||
/// <param name="playlistItemId">The item id.</param>
|
||||
/// <param name="visualIndex">The index at which the item should appear visually. The item with index 0 is at the top of the list.</param>
|
||||
private void assertItemInHistoryListStep(int playlistItemId, int visualIndex)
|
||||
{
|
||||
changeDisplayModeStep(MultiplayerPlaylistDisplayMode.History);
|
||||
|
||||
AddUntilStep($"{playlistItemId} in history at pos = {visualIndex}", () =>
|
||||
{
|
||||
return !inQueueList(playlistItemId)
|
||||
&& this.ChildrenOfType<MultiplayerHistoryList>()
|
||||
.Single()
|
||||
.ChildrenOfType<DrawableRoomPlaylistItem>()
|
||||
.OrderBy(drawable => drawable.Position.Y)
|
||||
.TakeWhile(drawable => drawable.Item.ID != playlistItemId)
|
||||
.Count() == visualIndex;
|
||||
});
|
||||
}
|
||||
|
||||
private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode);
|
||||
|
||||
private bool inQueueList(int playlistItemId)
|
||||
{
|
||||
return this.ChildrenOfType<MultiplayerQueueList>()
|
||||
.Single()
|
||||
.Items.Any(i => i.ID == playlistItemId);
|
||||
}
|
||||
|
||||
private bool inHistoryList(int playlistItemId)
|
||||
{
|
||||
return this.ChildrenOfType<MultiplayerHistoryList>()
|
||||
.Single()
|
||||
.Items.Any(i => i.ID == playlistItemId);
|
||||
}
|
||||
}
|
||||
}
|
@ -32,12 +32,36 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// </summary>
|
||||
public event Action? RoomUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new user joins the room.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerRoomUser>? UserJoined;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a user leaves the room of their own accord.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerRoomUser>? UserLeft;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a user was kicked from the room forcefully.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerRoomUser>? UserKicked;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a new item is added to the playlist.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerPlaylistItem>? ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a playlist item is removed from the playlist. The provided <c>long</c> is the playlist's item ID.
|
||||
/// </summary>
|
||||
public event Action<long>? ItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a playlist item's details change.
|
||||
/// </summary>
|
||||
public event Action<MultiplayerPlaylistItem>? ItemChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
|
||||
/// </summary>
|
||||
@ -617,6 +641,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
Room.Playlist.Add(item);
|
||||
APIRoom.Playlist.Add(playlistItem);
|
||||
|
||||
ItemAdded?.Invoke(item);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
}
|
||||
@ -636,6 +661,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId));
|
||||
APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId);
|
||||
|
||||
ItemRemoved?.Invoke(playlistItemId);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
|
||||
@ -666,6 +692,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
if (CurrentMatchPlayingItem.Value?.ID == playlistItem.ID)
|
||||
CurrentMatchPlayingItem.Value = playlistItem;
|
||||
|
||||
ItemChanged?.Invoke(item);
|
||||
RoomUpdated?.Invoke();
|
||||
});
|
||||
}
|
||||
@ -714,7 +741,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
OwnerID = item.OwnerID,
|
||||
Beatmap = { Value = apiBeatmap },
|
||||
Ruleset = { Value = ruleset },
|
||||
Expired = item.Expired
|
||||
Expired = item.Expired,
|
||||
PlaylistOrder = item.PlaylistOrder,
|
||||
PlayedAt = item.PlayedAt
|
||||
};
|
||||
|
||||
playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
|
||||
|
@ -39,6 +39,18 @@ namespace osu.Game.Online.Rooms
|
||||
[Key(7)]
|
||||
public bool Expired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The order in which this <see cref="MultiplayerPlaylistItem"/> will be played, starting from 0 and increasing for items which will be played later.
|
||||
/// </summary>
|
||||
[Key(8)]
|
||||
public ushort PlaylistOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date when this <see cref="MultiplayerPlaylistItem"/> was played.
|
||||
/// </summary>
|
||||
[Key(9)]
|
||||
public DateTimeOffset? PlayedAt { get; set; }
|
||||
|
||||
public MultiplayerPlaylistItem()
|
||||
{
|
||||
}
|
||||
@ -52,6 +64,8 @@ namespace osu.Game.Online.Rooms
|
||||
RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray();
|
||||
AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray();
|
||||
Expired = item.Expired;
|
||||
PlaylistOrder = item.PlaylistOrder ?? 0;
|
||||
PlayedAt = item.PlayedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,12 @@ namespace osu.Game.Online.Rooms
|
||||
[JsonProperty("expired")]
|
||||
public bool Expired { get; set; }
|
||||
|
||||
[JsonProperty("playlist_order")]
|
||||
public ushort? PlaylistOrder { get; set; }
|
||||
|
||||
[JsonProperty("played_at")]
|
||||
public DateTimeOffset? PlayedAt { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IBindable<bool> Valid => valid;
|
||||
|
||||
|
@ -1,9 +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.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -22,13 +20,11 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
private readonly bool allowSelection;
|
||||
private readonly bool showItemOwner;
|
||||
|
||||
public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool reverse = false, bool showItemOwner = false)
|
||||
public DrawableRoomPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false)
|
||||
{
|
||||
this.allowEdit = allowEdit;
|
||||
this.allowSelection = allowSelection;
|
||||
this.showItemOwner = showItemOwner;
|
||||
|
||||
((ReversibleFillFlowContainer)ListContainer).Reverse = reverse;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -53,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
d.ScrollbarVisible = false;
|
||||
});
|
||||
|
||||
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => new ReversibleFillFlowContainer
|
||||
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => new FillFlowContainer<RearrangeableListItem<PlaylistItem>>
|
||||
{
|
||||
Spacing = new Vector2(0, 2)
|
||||
};
|
||||
@ -76,22 +72,5 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
Items.Remove(item);
|
||||
}
|
||||
|
||||
private class ReversibleFillFlowContainer : FillFlowContainer<RearrangeableListItem<PlaylistItem>>
|
||||
{
|
||||
private bool reverse;
|
||||
|
||||
public bool Reverse
|
||||
{
|
||||
get => reverse;
|
||||
set
|
||||
{
|
||||
reverse = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<Drawable> FlowingChildren => Reverse ? base.FlowingChildren.OrderBy(d => -GetLayoutPosition(d)) : base.FlowingChildren;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
{
|
||||
/// <summary>
|
||||
/// A historically-ordered list of <see cref="DrawableRoomPlaylistItem"/>s.
|
||||
/// </summary>
|
||||
public class MultiplayerHistoryList : DrawableRoomPlaylist
|
||||
{
|
||||
public MultiplayerHistoryList()
|
||||
: base(false, false, true)
|
||||
{
|
||||
}
|
||||
|
||||
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => new HistoryFillFlowContainer
|
||||
{
|
||||
Spacing = new Vector2(0, 2)
|
||||
};
|
||||
|
||||
private class HistoryFillFlowContainer : FillFlowContainer<RearrangeableListItem<PlaylistItem>>
|
||||
{
|
||||
public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.OfType<RearrangeableListItem<PlaylistItem>>().OrderByDescending(item => item.Model.PlayedAt);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
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 class MultiplayerPlaylist : MultiplayerRoomComposite
|
||||
{
|
||||
public readonly Bindable<MultiplayerPlaylistDisplayMode> DisplayMode = new Bindable<MultiplayerPlaylistDisplayMode>();
|
||||
|
||||
private MultiplayerQueueList queueList;
|
||||
private MultiplayerHistoryList historyList;
|
||||
private bool firstPopulation = true;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float tab_control_height = 25;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new OsuTabControl<MultiplayerPlaylistDisplayMode>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = tab_control_height,
|
||||
Current = { BindTarget = DisplayMode }
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = tab_control_height + 5 },
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
queueList = new MultiplayerQueueList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
SelectedItem = { BindTarget = SelectedItem }
|
||||
},
|
||||
historyList = new MultiplayerHistoryList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
SelectedItem = { BindTarget = SelectedItem }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
DisplayMode.BindValueChanged(onDisplayModeChanged, true);
|
||||
}
|
||||
|
||||
private void onDisplayModeChanged(ValueChangedEvent<MultiplayerPlaylistDisplayMode> mode)
|
||||
{
|
||||
historyList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.History ? 1 : 0, 100);
|
||||
queueList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.Queue ? 1 : 0, 100);
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
|
||||
if (Room == null)
|
||||
{
|
||||
historyList.Items.Clear();
|
||||
queueList.Items.Clear();
|
||||
firstPopulation = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstPopulation)
|
||||
{
|
||||
foreach (var item in Room.Playlist)
|
||||
addItemToLists(item);
|
||||
|
||||
firstPopulation = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PlaylistItemAdded(MultiplayerPlaylistItem item)
|
||||
{
|
||||
base.PlaylistItemAdded(item);
|
||||
addItemToLists(item);
|
||||
}
|
||||
|
||||
protected override void PlaylistItemRemoved(long item)
|
||||
{
|
||||
base.PlaylistItemRemoved(item);
|
||||
removeItemFromLists(item);
|
||||
}
|
||||
|
||||
protected override void PlaylistItemChanged(MultiplayerPlaylistItem item)
|
||||
{
|
||||
base.PlaylistItemChanged(item);
|
||||
|
||||
removeItemFromLists(item.ID);
|
||||
addItemToLists(item);
|
||||
}
|
||||
|
||||
private void addItemToLists(MultiplayerPlaylistItem item)
|
||||
{
|
||||
var apiItem = Playlist.Single(i => i.ID == item.ID);
|
||||
|
||||
if (item.Expired)
|
||||
historyList.Items.Add(apiItem);
|
||||
else
|
||||
queueList.Items.Add(apiItem);
|
||||
}
|
||||
|
||||
private void removeItemFromLists(long item)
|
||||
{
|
||||
queueList.Items.RemoveAll(i => i.ID == item);
|
||||
historyList.Items.RemoveAll(i => i.ID == item);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of list displayed in a <see cref="MultiplayerPlaylist"/>.
|
||||
/// </summary>
|
||||
public enum MultiplayerPlaylistDisplayMode
|
||||
{
|
||||
Queue,
|
||||
History,
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
{
|
||||
/// <summary>
|
||||
/// A gameplay-ordered list of <see cref="DrawableRoomPlaylistItem"/>s.
|
||||
/// </summary>
|
||||
public class MultiplayerQueueList : DrawableRoomPlaylist
|
||||
{
|
||||
public MultiplayerQueueList()
|
||||
: base(false, false, true)
|
||||
{
|
||||
}
|
||||
|
||||
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => new QueueFillFlowContainer
|
||||
{
|
||||
Spacing = new Vector2(0, 2)
|
||||
};
|
||||
|
||||
private class QueueFillFlowContainer : FillFlowContainer<RearrangeableListItem<PlaylistItem>>
|
||||
{
|
||||
[Resolved(typeof(Room), nameof(Room.Playlist))]
|
||||
private BindableList<PlaylistItem> roomPlaylist { get; set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
roomPlaylist.BindCollectionChanged((_, __) => InvalidateLayout());
|
||||
}
|
||||
|
||||
public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.OfType<RearrangeableListItem<PlaylistItem>>().OrderBy(item => item.Model.PlaylistOrder);
|
||||
}
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -56,8 +57,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
[CanBeNull]
|
||||
private IDisposable readyClickOperation;
|
||||
|
||||
private DrawableRoomPlaylist playlist;
|
||||
|
||||
public MultiplayerMatchSubScreen(Room room)
|
||||
: base(room)
|
||||
{
|
||||
@ -74,9 +73,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
|
||||
UserMods.BindValueChanged(onUserModsChanged);
|
||||
|
||||
playlist.Items.BindTo(Room.Playlist);
|
||||
playlist.SelectedItem.BindTo(SelectedItem);
|
||||
|
||||
client.LoadRequested += onLoadRequested;
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
|
||||
@ -153,10 +149,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
null,
|
||||
new Drawable[]
|
||||
{
|
||||
playlist = new DrawableRoomPlaylist(false, false, true, true)
|
||||
new MultiplayerPlaylist
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
},
|
||||
new[]
|
||||
{
|
||||
|
@ -4,6 +4,7 @@
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
@ -23,6 +24,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Client.UserLeft += invokeUserLeft;
|
||||
Client.UserKicked += invokeUserKicked;
|
||||
Client.UserJoined += invokeUserJoined;
|
||||
Client.ItemAdded += invokeItemAdded;
|
||||
Client.ItemRemoved += invokeItemRemoved;
|
||||
Client.ItemChanged += invokeItemChanged;
|
||||
|
||||
OnRoomUpdated();
|
||||
}
|
||||
@ -31,6 +35,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user);
|
||||
private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user);
|
||||
private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(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));
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a user has joined the room.
|
||||
@ -56,6 +63,30 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@ -71,6 +102,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Client.UserLeft -= invokeUserLeft;
|
||||
Client.UserKicked -= invokeUserKicked;
|
||||
Client.UserJoined -= invokeUserJoined;
|
||||
Client.ItemAdded -= invokeItemAdded;
|
||||
Client.ItemRemoved -= invokeItemRemoved;
|
||||
Client.ItemChanged -= invokeItemChanged;
|
||||
}
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex];
|
||||
private int currentIndex;
|
||||
|
||||
private long lastPlaylistItemId;
|
||||
|
||||
public TestMultiplayerClient(TestMultiplayerRoomManager roomManager)
|
||||
{
|
||||
this.roomManager = roomManager;
|
||||
@ -145,7 +147,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
((IMultiplayerClient)this).ResultsReady();
|
||||
|
||||
finishCurrentItem().Wait();
|
||||
FinishCurrentItem().Wait();
|
||||
}
|
||||
|
||||
break;
|
||||
@ -169,6 +171,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
serverSidePlaylist.Clear();
|
||||
serverSidePlaylist.AddRange(apiRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)));
|
||||
lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID);
|
||||
|
||||
var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
|
||||
{
|
||||
@ -189,6 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Host = localUser
|
||||
};
|
||||
|
||||
await updatePlaylistOrder(room).ConfigureAwait(false);
|
||||
await updateCurrentItem(room, false).ConfigureAwait(false);
|
||||
|
||||
RoomSetupAction?.Invoke(room);
|
||||
@ -308,12 +312,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
if (Room.Settings.QueueMode == QueueMode.HostOnly && Room.Host?.UserID != LocalUser?.UserID)
|
||||
throw new InvalidOperationException("Local user is not the room host.");
|
||||
|
||||
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.OwnerID = currentItem.OwnerID;
|
||||
item.PlaylistOrder = currentItem.PlaylistOrder;
|
||||
|
||||
serverSidePlaylist[currentIndex] = item;
|
||||
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
|
||||
@ -323,12 +329,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
break;
|
||||
|
||||
default:
|
||||
item.ID = serverSidePlaylist.Last().ID + 1;
|
||||
item.OwnerID = userId;
|
||||
|
||||
serverSidePlaylist.Add(item);
|
||||
await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false);
|
||||
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;
|
||||
}
|
||||
@ -385,11 +388,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
if (newMode == QueueMode.HostOnly && serverSidePlaylist.All(item => item.Expired))
|
||||
await duplicateCurrentItem().ConfigureAwait(false);
|
||||
|
||||
// When changing modes, items could have been added (above) or the queueing order could have changed.
|
||||
await updatePlaylistOrder(Room).ConfigureAwait(false);
|
||||
await updateCurrentItem(Room).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task finishCurrentItem()
|
||||
public async Task FinishCurrentItem()
|
||||
{
|
||||
Debug.Assert(Room != null);
|
||||
Debug.Assert(APIRoom != null);
|
||||
@ -397,10 +400,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
// Expire the current playlist item.
|
||||
currentItem.Expired = true;
|
||||
currentItem.PlayedAt = DateTimeOffset.Now;
|
||||
|
||||
await ((IMultiplayerClient)this).PlaylistItemChanged(currentItem).ConfigureAwait(false);
|
||||
await updatePlaylistOrder(Room).ConfigureAwait(false);
|
||||
|
||||
// In host-only mode, a duplicate playlist item will be used for the next round.
|
||||
if (Room.Settings.QueueMode == QueueMode.HostOnly)
|
||||
if (Room.Settings.QueueMode == QueueMode.HostOnly && serverSidePlaylist.All(item => item.Expired))
|
||||
await duplicateCurrentItem().ConfigureAwait(false);
|
||||
|
||||
await updateCurrentItem(Room).ConfigureAwait(false);
|
||||
@ -408,47 +414,102 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private async Task duplicateCurrentItem()
|
||||
{
|
||||
Debug.Assert(Room != null);
|
||||
Debug.Assert(APIRoom != null);
|
||||
Debug.Assert(currentItem != null);
|
||||
|
||||
var newItem = new MultiplayerPlaylistItem
|
||||
await addItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = serverSidePlaylist.Last().ID + 1,
|
||||
BeatmapID = currentItem.BeatmapID,
|
||||
BeatmapChecksum = currentItem.BeatmapChecksum,
|
||||
RulesetID = currentItem.RulesetID,
|
||||
RequiredMods = currentItem.RequiredMods,
|
||||
AllowedMods = currentItem.AllowedMods
|
||||
};
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
serverSidePlaylist.Add(newItem);
|
||||
await ((IMultiplayerClient)this).PlaylistItemAdded(newItem).ConfigureAwait(false);
|
||||
private async Task addItem(MultiplayerPlaylistItem item)
|
||||
{
|
||||
Debug.Assert(Room != null);
|
||||
|
||||
// Add the item to the list first in order to compute gameplay order.
|
||||
serverSidePlaylist.Add(item);
|
||||
await updatePlaylistOrder(Room).ConfigureAwait(false);
|
||||
|
||||
item.ID = ++lastPlaylistItemId;
|
||||
await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true)
|
||||
{
|
||||
MultiplayerPlaylistItem newItem;
|
||||
MultiplayerPlaylistItem nextItem = serverSidePlaylist
|
||||
.Where(i => !i.Expired)
|
||||
.OrderBy(i => i.PlaylistOrder)
|
||||
.FirstOrDefault()
|
||||
?? room.Playlist.Last();
|
||||
|
||||
currentIndex = serverSidePlaylist.IndexOf(nextItem);
|
||||
|
||||
long lastItem = room.Settings.PlaylistItemId;
|
||||
room.Settings.PlaylistItemId = nextItem.ID;
|
||||
|
||||
if (notify && nextItem.ID != lastItem)
|
||||
await ((IMultiplayerClient)this).SettingsChanged(room.Settings).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task updatePlaylistOrder(MultiplayerRoom room)
|
||||
{
|
||||
List<MultiplayerPlaylistItem> orderedActiveItems;
|
||||
|
||||
switch (room.Settings.QueueMode)
|
||||
{
|
||||
default:
|
||||
// Pick the single non-expired playlist item.
|
||||
newItem = serverSidePlaylist.FirstOrDefault(i => !i.Expired) ?? serverSidePlaylist.Last();
|
||||
orderedActiveItems = serverSidePlaylist.Where(item => !item.Expired).OrderBy(item => item.ID == 0 ? int.MaxValue : item.ID).ToList();
|
||||
break;
|
||||
|
||||
case QueueMode.AllPlayersRoundRobin:
|
||||
// Group playlist items by (user_id -> count_expired), and select the first available playlist item from a user that has available beatmaps where count_expired is the lowest.
|
||||
throw new NotImplementedException();
|
||||
orderedActiveItems = new List<MultiplayerPlaylistItem>();
|
||||
|
||||
// Todo: This could probably be more efficient, likely at the cost of increased complexity.
|
||||
// Number of "expired" or "used" items per player.
|
||||
Dictionary<int, int> perUserCounts = serverSidePlaylist
|
||||
.GroupBy(item => item.OwnerID)
|
||||
.ToDictionary(group => group.Key, group => group.Count(item => item.Expired));
|
||||
|
||||
// We'll run a simulation over all items which are not expired ("unprocessed"). Expired items will not have their ordering updated.
|
||||
List<MultiplayerPlaylistItem> unprocessedItems = serverSidePlaylist.Where(item => !item.Expired).ToList();
|
||||
|
||||
// In every iteration of the simulation, pick the first available item from the user with the lowest number of items in the queue to add to the result set.
|
||||
// If multiple users have the same number of items in the queue, then the item with the lowest ID is chosen.
|
||||
while (unprocessedItems.Count > 0)
|
||||
{
|
||||
MultiplayerPlaylistItem candidateItem = unprocessedItems
|
||||
.OrderBy(item => perUserCounts[item.OwnerID])
|
||||
.ThenBy(item => item.ID == 0 ? int.MaxValue : item.ID)
|
||||
.First();
|
||||
|
||||
unprocessedItems.Remove(candidateItem);
|
||||
orderedActiveItems.Add(candidateItem);
|
||||
|
||||
perUserCounts[candidateItem.OwnerID]++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
currentIndex = serverSidePlaylist.IndexOf(newItem);
|
||||
for (int i = 0; i < orderedActiveItems.Count; i++)
|
||||
{
|
||||
var item = orderedActiveItems[i];
|
||||
|
||||
long lastItem = room.Settings.PlaylistItemId;
|
||||
room.Settings.PlaylistItemId = newItem.ID;
|
||||
if (item.PlaylistOrder == i)
|
||||
continue;
|
||||
|
||||
if (notify && newItem.ID != lastItem)
|
||||
await ((IMultiplayerClient)this).SettingsChanged(room.Settings).ConfigureAwait(false);
|
||||
item.PlaylistOrder = (ushort)i;
|
||||
|
||||
// Items which have an ID of 0 are not in the database, so avoid propagating database/hub events for them.
|
||||
if (item.ID <= 0)
|
||||
continue;
|
||||
|
||||
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user