2020-12-19 00:14:50 +08:00
// 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.
2021-03-01 16:24:54 +08:00
using System ;
2021-02-01 16:54:56 +08:00
using System.Collections.Generic ;
2020-12-19 00:14:50 +08:00
using System.Diagnostics ;
using System.Linq ;
using System.Threading.Tasks ;
2022-07-01 17:03:26 +08:00
using MessagePack ;
2020-12-19 00:14:50 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Bindables ;
2022-01-03 16:02:15 +08:00
using osu.Framework.Extensions ;
2022-07-01 17:03:26 +08:00
using osu.Game.Online ;
2020-12-19 00:14:50 +08:00
using osu.Game.Online.API ;
2022-04-13 20:24:33 +08:00
using osu.Game.Online.API.Requests.Responses ;
2020-12-25 12:38:11 +08:00
using osu.Game.Online.Multiplayer ;
2021-08-02 16:06:01 +08:00
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus ;
2021-01-03 09:32:50 +08:00
using osu.Game.Online.Rooms ;
2021-02-01 16:54:56 +08:00
using osu.Game.Rulesets.Mods ;
2020-12-19 00:14:50 +08:00
2020-12-25 12:38:11 +08:00
namespace osu.Game.Tests.Visual.Multiplayer
2020-12-19 00:14:50 +08:00
{
2021-06-25 17:02:53 +08:00
/// <summary>
/// A <see cref="MultiplayerClient"/> for use in multiplayer test scenes. Should generally not be used by itself outside of a <see cref="MultiplayerTestScene"/>.
/// </summary>
2021-05-20 14:39:45 +08:00
public partial class TestMultiplayerClient : MultiplayerClient
2020-12-19 00:14:50 +08:00
{
2020-12-19 00:16:00 +08:00
public override IBindable < bool > IsConnected = > isConnected ;
private readonly Bindable < bool > isConnected = new Bindable < bool > ( true ) ;
2020-12-19 00:14:50 +08:00
2022-04-01 17:31:17 +08:00
/// <summary>
/// The local client's <see cref="Room"/>. This is not always equivalent to the server-side room.
/// </summary>
2022-07-01 17:58:22 +08:00
public Room ? ClientAPIRoom = > base . APIRoom ;
/// <summary>
/// The local client's <see cref="MultiplayerRoom"/>. This is not always equivalent to the server-side room.
/// </summary>
public MultiplayerRoom ? ClientRoom = > base . Room ;
/// <summary>
/// The server's <see cref="Room"/>. This is always up-to-date.
/// </summary>
public Room ? ServerAPIRoom { get ; private set ; }
/// <summary>
/// The server's <see cref="MultiplayerRoom"/>. This is always up-to-date.
/// </summary>
public MultiplayerRoom ? ServerRoom { get ; private set ; }
[Obsolete]
protected new Room APIRoom = > throw new InvalidOperationException ( $"Accessing the client-side API room via {nameof(TestMultiplayerClient)} is unsafe. "
+ $"Use {nameof(ClientAPIRoom)} if this was intended." ) ;
[Obsolete]
public new MultiplayerRoom Room = > throw new InvalidOperationException ( $"Accessing the client-side room via {nameof(TestMultiplayerClient)} is unsafe. "
+ $"Use {nameof(ClientRoom)} if this was intended." ) ;
2022-04-01 17:31:17 +08:00
2022-07-01 18:15:31 +08:00
public new MultiplayerRoomUser ? LocalUser = > ServerRoom ? . Users . SingleOrDefault ( u = > u . User ? . Id = = API . LocalUser . Value . Id ) ;
2021-04-22 22:22:44 +08:00
public Action < MultiplayerRoom > ? RoomSetupAction ;
2021-12-20 12:10:13 +08:00
public bool RoomJoined { get ; private set ; }
2020-12-19 00:14:50 +08:00
[Resolved]
private IAPIProvider api { get ; set ; } = null ! ;
2021-10-27 15:10:22 +08:00
private readonly TestMultiplayerRoomManager roomManager ;
2021-11-22 10:26:41 +08:00
2022-07-01 17:58:22 +08:00
private MultiplayerPlaylistItem ? currentItem = > ServerRoom ? . Playlist [ currentIndex ] ;
2021-11-16 13:37:29 +08:00
private int currentIndex ;
2021-12-03 15:48:54 +08:00
private long lastPlaylistItemId ;
2021-10-27 15:10:22 +08:00
public TestMultiplayerClient ( TestMultiplayerRoomManager roomManager )
2021-03-03 18:40:19 +08:00
{
this . roomManager = roomManager ;
}
2020-12-19 00:16:00 +08:00
public void Connect ( ) = > isConnected . Value = true ;
public void Disconnect ( ) = > isConnected . Value = false ;
2021-11-04 17:02:44 +08:00
public MultiplayerRoomUser AddUser ( APIUser user , bool markAsPlaying = false )
2022-09-07 19:00:24 +08:00
= > AddUser ( new MultiplayerRoomUser ( user . Id ) { User = user } , markAsPlaying ) ;
2021-10-14 23:10:39 +08:00
2022-09-07 19:00:24 +08:00
public MultiplayerRoomUser AddUser ( MultiplayerRoomUser roomUser , bool markAsPlaying = false )
{
2021-10-14 23:10:39 +08:00
addUser ( roomUser ) ;
2021-08-09 18:18:13 +08:00
if ( markAsPlaying )
2022-09-07 19:00:24 +08:00
PlayingUserIds . Add ( roomUser . UserID ) ;
2021-08-09 18:18:13 +08:00
return roomUser ;
}
2020-12-19 00:14:50 +08:00
2021-11-02 15:51:27 +08:00
public void TestAddUnresolvedUser ( ) = > addUser ( new MultiplayerRoomUser ( TestUserLookupCache . UNRESOLVED_USER_ID ) ) ;
2021-10-14 23:10:39 +08:00
private void addUser ( MultiplayerRoomUser user )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-10-14 23:10:39 +08:00
2022-07-01 17:58:22 +08:00
ServerRoom . Users . Add ( user ) ;
( ( IMultiplayerClient ) this ) . UserJoined ( clone ( user ) ) . WaitSafely ( ) ;
2021-11-11 15:39:59 +08:00
2022-07-01 17:58:22 +08:00
switch ( ServerRoom ? . MatchState )
2021-11-11 15:39:59 +08:00
{
case TeamVersusRoomState teamVersus :
// simulate the server's automatic assignment of users to teams on join.
// the "best" team is the one with the least users on it.
int bestTeam = teamVersus . Teams
2023-05-25 19:09:40 +08:00
. Select ( team = > ( teamID : team . ID , userCount : ServerRoom . Users . Count ( u = > ( u . MatchState as TeamVersusUserState ) ? . TeamID = = team . ID ) ) )
. MinBy ( pair = > pair . userCount ) . teamID ;
2022-07-01 17:58:22 +08:00
user . MatchState = new TeamVersusUserState { TeamID = bestTeam } ;
( ( IMultiplayerClient ) this ) . MatchUserStateChanged ( clone ( user . UserID ) , clone ( user . MatchState ) ) . WaitSafely ( ) ;
2021-11-11 15:39:59 +08:00
break ;
}
2021-10-14 23:10:39 +08:00
}
2020-12-28 13:56:53 +08:00
2021-11-04 17:02:44 +08:00
public void RemoveUser ( APIUser user )
2020-12-19 00:14:50 +08:00
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2022-06-23 16:38:34 +08:00
2022-07-01 17:58:22 +08:00
ServerRoom . Users . Remove ( ServerRoom . Users . Single ( u = > u . UserID = = user . Id ) ) ;
2022-07-01 17:03:26 +08:00
( ( IMultiplayerClient ) this ) . UserLeft ( clone ( new MultiplayerRoomUser ( user . Id ) ) ) ;
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
if ( ServerRoom . Users . Any ( ) )
TransferHost ( ServerRoom . Users . First ( ) . UserID ) ;
2020-12-19 00:14:50 +08:00
}
2021-04-07 15:35:36 +08:00
public void ChangeRoomState ( MultiplayerRoomState newState )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
ServerRoom . State = clone ( newState ) ;
( ( IMultiplayerClient ) this ) . RoomStateChanged ( clone ( ServerRoom . State ) ) ;
2021-04-07 15:35:36 +08:00
}
2020-12-19 00:14:50 +08:00
public void ChangeUserState ( int userId , MultiplayerUserState newState )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
var user = ServerRoom . Users . Single ( u = > u . UserID = = userId ) ;
user . State = clone ( newState ) ;
( ( IMultiplayerClient ) this ) . UserStateChanged ( clone ( userId ) , clone ( user . State ) ) ;
2022-03-24 19:16:43 +08:00
updateRoomStateIfRequired ( ) ;
}
private void updateRoomStateIfRequired ( )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
switch ( ServerRoom . State )
2020-12-19 00:14:50 +08:00
{
2022-07-01 17:58:22 +08:00
case MultiplayerRoomState . Open :
break ;
2022-06-23 16:38:34 +08:00
2022-07-01 17:58:22 +08:00
case MultiplayerRoomState . WaitingForLoad :
if ( ServerRoom . Users . All ( u = > u . State ! = MultiplayerUserState . WaitingForLoad ) )
{
var loadedUsers = ServerRoom . Users . Where ( u = > u . State = = MultiplayerUserState . Loaded ) . ToArray ( ) ;
2022-03-17 18:26:42 +08:00
2022-07-01 17:58:22 +08:00
if ( loadedUsers . Length = = 0 )
2020-12-19 00:14:50 +08:00
{
2022-07-01 17:58:22 +08:00
// all users have bailed from the load sequence. cancel the game start.
ChangeRoomState ( MultiplayerRoomState . Open ) ;
return ;
}
2021-12-14 10:35:56 +08:00
2022-07-01 17:58:22 +08:00
foreach ( var u in ServerRoom . Users . Where ( u = > u . State = = MultiplayerUserState . Loaded ) )
ChangeUserState ( u . UserID , MultiplayerUserState . Playing ) ;
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
( ( IMultiplayerClient ) this ) . GameplayStarted ( ) ;
2021-10-22 20:16:10 +08:00
2022-07-01 17:58:22 +08:00
ChangeRoomState ( MultiplayerRoomState . Playing ) ;
}
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
break ;
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
case MultiplayerRoomState . Playing :
if ( ServerRoom . Users . All ( u = > u . State ! = MultiplayerUserState . Playing ) )
{
foreach ( var u in ServerRoom . Users . Where ( u = > u . State = = MultiplayerUserState . FinishedPlay ) )
ChangeUserState ( u . UserID , MultiplayerUserState . Results ) ;
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
ChangeRoomState ( MultiplayerRoomState . Open ) ;
( ( IMultiplayerClient ) this ) . ResultsReady ( ) ;
2021-10-22 21:07:41 +08:00
2022-07-01 17:58:22 +08:00
FinishCurrentItem ( ) . WaitSafely ( ) ;
}
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
break ;
}
2020-12-19 00:14:50 +08:00
}
2021-01-03 09:32:50 +08:00
public void ChangeUserBeatmapAvailability ( int userId , BeatmapAvailability newBeatmapAvailability )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
var user = ServerRoom . Users . Single ( u = > u . UserID = = userId ) ;
2022-07-01 18:36:37 +08:00
user . BeatmapAvailability = newBeatmapAvailability ;
2022-07-01 17:58:22 +08:00
( ( IMultiplayerClient ) this ) . UserBeatmapAvailabilityChanged ( clone ( userId ) , clone ( user . BeatmapAvailability ) ) ;
2021-01-03 09:32:50 +08:00
}
2021-11-13 00:42:51 +08:00
protected override async Task < MultiplayerRoom > JoinRoom ( long roomId , string? password = null )
2020-12-19 00:14:50 +08:00
{
2024-08-19 03:04:24 +08:00
if ( RoomJoined | | ServerAPIRoom ! = null )
throw new InvalidOperationException ( "Already joined a room" ) ;
2022-07-01 18:36:37 +08:00
roomId = clone ( roomId ) ;
password = clone ( password ) ;
2022-07-01 17:58:22 +08:00
ServerAPIRoom = roomManager . ServerSideRooms . Single ( r = > r . RoomID . Value = = roomId ) ;
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
if ( password ! = ServerAPIRoom . Password . Value )
2021-07-19 19:01:44 +08:00
throw new InvalidOperationException ( "Invalid password." ) ;
2022-07-01 17:58:22 +08:00
lastPlaylistItemId = ServerAPIRoom . Playlist . Max ( item = > item . ID ) ;
2021-11-22 10:18:21 +08:00
2021-04-22 22:22:44 +08:00
var localUser = new MultiplayerRoomUser ( api . LocalUser . Value . Id )
2021-03-01 16:24:32 +08:00
{
User = api . LocalUser . Value
} ;
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
ServerRoom = new MultiplayerRoom ( roomId )
2021-03-01 16:24:32 +08:00
{
Settings =
{
2022-07-01 17:58:22 +08:00
Name = ServerAPIRoom . Name . Value ,
MatchType = ServerAPIRoom . Type . Value ,
2021-10-22 19:14:04 +08:00
Password = password ,
2022-07-01 17:58:22 +08:00
QueueMode = ServerAPIRoom . QueueMode . Value ,
AutoStartDuration = ServerAPIRoom . AutoStartDuration . Value
2021-03-01 16:24:32 +08:00
} ,
2023-06-03 22:50:58 +08:00
Playlist = ServerAPIRoom . Playlist . Select ( CreateMultiplayerPlaylistItem ) . ToList ( ) ,
2021-04-22 22:22:44 +08:00
Users = { localUser } ,
Host = localUser
2021-03-01 16:24:32 +08:00
} ;
2020-12-19 00:14:50 +08:00
2022-07-01 17:58:22 +08:00
await updatePlaylistOrder ( ServerRoom ) . ConfigureAwait ( false ) ;
await updateCurrentItem ( ServerRoom , false ) . ConfigureAwait ( false ) ;
2021-11-16 13:37:29 +08:00
2022-07-01 17:58:22 +08:00
RoomSetupAction ? . Invoke ( ServerRoom ) ;
2021-04-22 22:22:44 +08:00
RoomSetupAction = null ;
2022-07-01 17:58:22 +08:00
return clone ( ServerRoom ) ;
2020-12-19 00:14:50 +08:00
}
2021-08-03 16:08:19 +08:00
protected override void OnRoomJoined ( )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-08-03 16:08:19 +08:00
// emulate the server sending this after the join room. scheduler required to make sure the join room event is fired first (in Join).
2022-07-01 17:58:22 +08:00
changeMatchType ( ServerRoom . Settings . MatchType ) . WaitSafely ( ) ;
2021-12-20 12:10:13 +08:00
RoomJoined = true ;
2021-08-03 16:08:19 +08:00
}
2021-12-20 12:10:13 +08:00
protected override Task LeaveRoomInternal ( )
{
RoomJoined = false ;
2024-08-19 03:04:24 +08:00
ServerAPIRoom = null ;
ServerRoom = null ;
2021-12-20 12:10:13 +08:00
return Task . CompletedTask ;
}
2021-01-25 19:41:51 +08:00
2023-10-03 04:53:28 +08:00
public override Task InvitePlayer ( int userId )
{
return Task . CompletedTask ;
}
2022-07-01 17:58:22 +08:00
public override Task TransferHost ( int userId )
{
2022-07-01 18:36:37 +08:00
userId = clone ( userId ) ;
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
ServerRoom . Host = ServerRoom . Users . Single ( u = > u . UserID = = userId ) ;
return ( ( IMultiplayerClient ) this ) . HostChanged ( clone ( userId ) ) ;
}
2020-12-19 00:14:50 +08:00
2021-08-11 16:20:41 +08:00
public override Task KickUser ( int userId )
{
2022-07-01 18:36:37 +08:00
userId = clone ( userId ) ;
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
var user = ServerRoom . Users . Single ( u = > u . UserID = = userId ) ;
ServerRoom . Users . Remove ( user ) ;
return ( ( IMultiplayerClient ) this ) . UserKicked ( clone ( user ) ) ;
2021-08-11 16:20:41 +08:00
}
2020-12-19 00:14:50 +08:00
public override async Task ChangeSettings ( MultiplayerRoomSettings settings )
{
2022-07-01 18:36:37 +08:00
settings = clone ( settings ) ;
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-11-13 00:42:51 +08:00
Debug . Assert ( currentItem ! = null ) ;
2021-10-22 21:07:41 +08:00
2021-11-11 22:39:15 +08:00
// Server is authoritative for the time being.
2022-07-01 17:58:22 +08:00
settings . PlaylistItemId = ServerRoom . Settings . PlaylistItemId ;
ServerRoom . Settings = settings ;
2020-12-19 00:14:50 +08:00
2021-11-13 00:42:51 +08:00
await changeQueueMode ( settings . QueueMode ) . ConfigureAwait ( false ) ;
2022-07-01 17:03:26 +08:00
await ( ( IMultiplayerClient ) this ) . SettingsChanged ( clone ( settings ) ) . ConfigureAwait ( false ) ;
2021-11-13 00:42:51 +08:00
2022-07-01 17:58:22 +08:00
foreach ( var user in ServerRoom . Users . Where ( u = > u . State = = MultiplayerUserState . Ready ) )
2021-11-13 00:42:51 +08:00
ChangeUserState ( user . UserID , MultiplayerUserState . Idle ) ;
await changeMatchType ( settings . MatchType ) . ConfigureAwait ( false ) ;
2022-03-24 19:16:43 +08:00
updateRoomStateIfRequired ( ) ;
2020-12-19 00:14:50 +08:00
}
public override Task ChangeState ( MultiplayerUserState newState )
{
2022-07-01 18:36:37 +08:00
newState = clone ( newState ) ;
2021-12-14 10:30:42 +08:00
if ( newState = = MultiplayerUserState . Idle & & LocalUser ? . State = = MultiplayerUserState . WaitingForLoad )
return Task . CompletedTask ;
2022-07-01 18:36:37 +08:00
ChangeUserState ( api . LocalUser . Value . Id , clone ( newState ) ) ;
2020-12-19 00:14:50 +08:00
return Task . CompletedTask ;
}
2021-01-03 09:32:50 +08:00
public override Task ChangeBeatmapAvailability ( BeatmapAvailability newBeatmapAvailability )
{
2022-07-01 18:36:37 +08:00
ChangeUserBeatmapAvailability ( api . LocalUser . Value . Id , clone ( newBeatmapAvailability ) ) ;
2021-01-03 09:32:50 +08:00
return Task . CompletedTask ;
}
2021-02-01 16:57:32 +08:00
public void ChangeUserMods ( int userId , IEnumerable < Mod > newMods )
2022-07-01 18:36:37 +08:00
= > ChangeUserMods ( userId , newMods . Select ( m = > new APIMod ( m ) ) ) ;
2021-02-01 16:54:56 +08:00
2021-02-01 16:57:32 +08:00
public void ChangeUserMods ( int userId , IEnumerable < APIMod > newMods )
2021-02-01 16:54:56 +08:00
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
var user = ServerRoom . Users . Single ( u = > u . UserID = = userId ) ;
2022-07-01 18:36:37 +08:00
user . Mods = newMods . ToArray ( ) ;
2022-07-01 17:58:22 +08:00
( ( IMultiplayerClient ) this ) . UserModsChanged ( clone ( userId ) , clone ( user . Mods ) ) ;
2021-02-01 16:54:56 +08:00
}
2021-02-01 16:57:32 +08:00
public override Task ChangeUserMods ( IEnumerable < APIMod > newMods )
2021-02-01 16:54:56 +08:00
{
2022-07-01 18:36:37 +08:00
ChangeUserMods ( api . LocalUser . Value . Id , clone ( newMods ) ) ;
2021-02-01 16:54:56 +08:00
return Task . CompletedTask ;
}
2021-08-02 16:06:01 +08:00
public override async Task SendMatchRequest ( MatchUserRequest request )
{
2022-07-01 18:36:37 +08:00
request = clone ( request ) ;
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-08-02 16:06:01 +08:00
Debug . Assert ( LocalUser ! = null ) ;
switch ( request )
{
case ChangeTeamRequest changeTeam :
2022-07-01 17:58:22 +08:00
TeamVersusRoomState roomState = ( TeamVersusRoomState ) ServerRoom . MatchState ! ;
2021-08-02 16:06:01 +08:00
TeamVersusUserState userState = ( TeamVersusUserState ) LocalUser . MatchState ! ;
var targetTeam = roomState . Teams . FirstOrDefault ( t = > t . ID = = changeTeam . TeamID ) ;
if ( targetTeam ! = null )
{
userState . TeamID = targetTeam . ID ;
2022-07-01 17:03:26 +08:00
await ( ( IMultiplayerClient ) this ) . MatchUserStateChanged ( clone ( LocalUser . UserID ) , clone ( userState ) ) . ConfigureAwait ( false ) ;
2021-08-02 16:06:01 +08:00
}
break ;
}
}
2021-07-21 18:13:56 +08:00
2022-03-24 14:07:01 +08:00
public override Task StartMatch ( )
2020-12-19 00:14:50 +08:00
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2020-12-19 00:14:50 +08:00
2021-04-07 19:46:30 +08:00
ChangeRoomState ( MultiplayerRoomState . WaitingForLoad ) ;
2022-07-01 17:58:22 +08:00
foreach ( var user in ServerRoom . Users . Where ( u = > u . State = = MultiplayerUserState . Ready ) )
2020-12-19 00:14:50 +08:00
ChangeUserState ( user . UserID , MultiplayerUserState . WaitingForLoad ) ;
2022-03-24 14:07:01 +08:00
return ( ( IMultiplayerClient ) this ) . LoadRequested ( ) ;
2020-12-19 00:14:50 +08:00
}
2021-03-01 16:24:54 +08:00
2021-12-14 15:52:57 +08:00
public override Task AbortGameplay ( )
2021-12-14 10:30:42 +08:00
{
Debug . Assert ( LocalUser ! = null ) ;
ChangeUserState ( LocalUser . UserID , MultiplayerUserState . Idle ) ;
return Task . CompletedTask ;
}
2023-12-01 20:34:20 +08:00
public override async Task AbortMatch ( )
2023-12-01 14:31:06 +08:00
{
2023-12-01 20:34:20 +08:00
ChangeUserState ( api . LocalUser . Value . Id , MultiplayerUserState . Idle ) ;
await ( ( IMultiplayerClient ) this ) . GameplayAborted ( GameplayAbortReason . HostAbortedTheMatch ) . ConfigureAwait ( false ) ;
2023-12-01 14:31:06 +08:00
}
2021-11-25 21:17:18 +08:00
public async Task AddUserPlaylistItem ( int userId , MultiplayerPlaylistItem item )
2021-10-22 15:48:28 +08:00
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-11-13 00:42:51 +08:00
Debug . Assert ( currentItem ! = null ) ;
2022-07-01 17:58:22 +08:00
if ( ServerRoom . Settings . QueueMode = = QueueMode . HostOnly & & ServerRoom . Host ? . UserID ! = LocalUser ? . UserID )
2021-11-13 00:42:51 +08:00
throw new InvalidOperationException ( "Local user is not the room host." ) ;
2021-10-22 15:48:28 +08:00
2021-12-02 21:32:41 +08:00
item . OwnerID = userId ;
2021-12-10 13:44:35 +08:00
await addItem ( item ) . ConfigureAwait ( false ) ;
2022-07-01 17:58:22 +08:00
await updateCurrentItem ( ServerRoom ) . ConfigureAwait ( false ) ;
2022-03-24 19:16:43 +08:00
updateRoomStateIfRequired ( ) ;
2021-12-10 13:44:35 +08:00
}
2021-11-16 13:37:29 +08:00
2022-07-01 17:58:22 +08:00
public override Task AddPlaylistItem ( MultiplayerPlaylistItem item ) = > AddUserPlaylistItem ( api . LocalUser . Value . OnlineID , clone ( item ) ) ;
2021-11-13 00:42:51 +08:00
2021-12-10 13:44:35 +08:00
public async Task EditUserPlaylistItem ( int userId , MultiplayerPlaylistItem item )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-12-10 13:44:35 +08:00
Debug . Assert ( currentItem ! = null ) ;
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerAPIRoom ! = null ) ;
2021-10-22 15:48:28 +08:00
2021-12-10 13:44:35 +08:00
item . OwnerID = userId ;
2021-11-16 13:37:29 +08:00
2022-07-01 17:58:22 +08:00
var existingItem = ServerRoom . Playlist . SingleOrDefault ( i = > i . ID = = item . ID ) ;
2021-12-10 00:03:36 +08:00
2021-12-10 13:44:35 +08:00
if ( existingItem = = null )
throw new InvalidOperationException ( "Attempted to change an item that doesn't exist." ) ;
2022-07-01 17:58:22 +08:00
if ( existingItem . OwnerID ! = userId & & ServerRoom . Host ? . UserID ! = LocalUser ? . UserID )
2021-12-10 13:44:35 +08:00
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 ;
2022-07-01 17:58:22 +08:00
ServerRoom . Playlist [ ServerRoom . Playlist . IndexOf ( existingItem ) ] = item ;
ServerAPIRoom . Playlist [ ServerAPIRoom . Playlist . IndexOf ( ServerAPIRoom . Playlist . Single ( i = > i . ID = = item . ID ) ) ] = new PlaylistItem ( item ) ;
2021-12-10 13:44:35 +08:00
2022-07-01 17:03:26 +08:00
await ( ( IMultiplayerClient ) this ) . PlaylistItemChanged ( clone ( item ) ) . ConfigureAwait ( false ) ;
2021-10-22 15:48:28 +08:00
}
2022-07-01 17:58:22 +08:00
public override Task EditPlaylistItem ( MultiplayerPlaylistItem item ) = > EditUserPlaylistItem ( api . LocalUser . Value . OnlineID , clone ( item ) ) ;
2021-11-25 21:17:18 +08:00
2021-12-09 01:12:33 +08:00
public async Task RemoveUserPlaylistItem ( int userId , long playlistItemId )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
Debug . Assert ( ServerAPIRoom ! = null ) ;
2021-12-09 01:12:33 +08:00
2022-07-01 17:58:22 +08:00
var item = ServerRoom . Playlist . FirstOrDefault ( i = > i . ID = = playlistItemId ) ;
2021-12-09 01:12:33 +08:00
if ( item = = null )
throw new InvalidOperationException ( "Item does not exist in the room." ) ;
if ( item = = currentItem )
throw new InvalidOperationException ( "The room's current item cannot be removed." ) ;
if ( item . OwnerID ! = userId )
throw new InvalidOperationException ( "Attempted to remove an item which is not owned by the user." ) ;
2021-12-09 03:12:24 +08:00
if ( item . Expired )
2021-12-09 03:18:53 +08:00
throw new InvalidOperationException ( "Attempted to remove an item which has already been played." ) ;
2021-12-09 03:12:24 +08:00
2022-07-01 17:58:22 +08:00
ServerRoom . Playlist . Remove ( item ) ;
ServerAPIRoom . Playlist . RemoveAll ( i = > i . ID = = item . ID ) ;
2022-07-01 17:03:26 +08:00
await ( ( IMultiplayerClient ) this ) . PlaylistItemRemoved ( clone ( playlistItemId ) ) . ConfigureAwait ( false ) ;
2021-12-09 01:12:33 +08:00
2022-07-01 17:58:22 +08:00
await updateCurrentItem ( ServerRoom ) . ConfigureAwait ( false ) ;
2022-03-24 19:16:43 +08:00
updateRoomStateIfRequired ( ) ;
2021-12-09 01:12:33 +08:00
}
2022-07-01 18:36:37 +08:00
public override Task RemovePlaylistItem ( long playlistItemId ) = > RemoveUserPlaylistItem ( api . LocalUser . Value . OnlineID , clone ( playlistItemId ) ) ;
2021-12-09 01:12:33 +08:00
2021-08-03 16:08:19 +08:00
private async Task changeMatchType ( MatchType type )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-08-03 16:08:19 +08:00
switch ( type )
{
case MatchType . HeadToHead :
2022-07-01 17:58:22 +08:00
ServerRoom . MatchState = null ;
await ( ( IMultiplayerClient ) this ) . MatchRoomStateChanged ( clone ( ServerRoom . MatchState ) ) . ConfigureAwait ( false ) ;
foreach ( var user in ServerRoom . Users )
{
user . MatchState = null ;
await ( ( IMultiplayerClient ) this ) . MatchUserStateChanged ( clone ( user . UserID ) , clone ( user . MatchState ) ) . ConfigureAwait ( false ) ;
}
2021-08-03 16:08:19 +08:00
break ;
case MatchType . TeamVersus :
2022-07-01 17:58:22 +08:00
ServerRoom . MatchState = TeamVersusRoomState . CreateDefault ( ) ;
await ( ( IMultiplayerClient ) this ) . MatchRoomStateChanged ( clone ( ServerRoom . MatchState ) ) . ConfigureAwait ( false ) ;
foreach ( var user in ServerRoom . Users )
{
user . MatchState = new TeamVersusUserState ( ) ;
await ( ( IMultiplayerClient ) this ) . MatchUserStateChanged ( clone ( user . UserID ) , clone ( user . MatchState ) ) . ConfigureAwait ( false ) ;
}
2021-08-03 16:08:19 +08:00
break ;
}
}
2021-10-22 21:07:41 +08:00
2021-11-16 13:53:10 +08:00
private async Task changeQueueMode ( QueueMode newMode )
2021-10-22 21:07:41 +08:00
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-11-13 00:42:51 +08:00
Debug . Assert ( currentItem ! = null ) ;
2021-10-22 21:07:41 +08:00
2021-11-22 10:10:08 +08:00
// When changing to host-only mode, ensure that at least one non-expired playlist item exists by duplicating the current item.
2022-07-01 17:58:22 +08:00
if ( newMode = = QueueMode . HostOnly & & ServerRoom . Playlist . All ( item = > item . Expired ) )
2021-11-22 10:10:08 +08:00
await duplicateCurrentItem ( ) . ConfigureAwait ( false ) ;
2021-10-22 21:07:41 +08:00
2022-07-01 17:58:22 +08:00
await updatePlaylistOrder ( ServerRoom ) . ConfigureAwait ( false ) ;
await updateCurrentItem ( ServerRoom ) . ConfigureAwait ( false ) ;
2021-10-22 21:07:41 +08:00
}
2021-12-01 17:44:46 +08:00
public async Task FinishCurrentItem ( )
2021-10-22 21:07:41 +08:00
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-11-13 00:42:51 +08:00
Debug . Assert ( currentItem ! = null ) ;
2021-10-22 21:07:41 +08:00
// Expire the current playlist item.
2021-11-13 02:34:45 +08:00
currentItem . Expired = true ;
2021-12-03 19:05:25 +08:00
currentItem . PlayedAt = DateTimeOffset . Now ;
2021-12-03 15:48:54 +08:00
2022-07-01 17:03:26 +08:00
await ( ( IMultiplayerClient ) this ) . PlaylistItemChanged ( clone ( currentItem ) ) . ConfigureAwait ( false ) ;
2022-07-01 17:58:22 +08:00
await updatePlaylistOrder ( ServerRoom ) . ConfigureAwait ( false ) ;
2021-10-22 21:07:41 +08:00
// In host-only mode, a duplicate playlist item will be used for the next round.
2022-07-01 17:58:22 +08:00
if ( ServerRoom . Settings . QueueMode = = QueueMode . HostOnly & & ServerRoom . Playlist . All ( item = > item . Expired ) )
2021-11-13 00:42:51 +08:00
await duplicateCurrentItem ( ) . ConfigureAwait ( false ) ;
2022-07-01 17:58:22 +08:00
await updateCurrentItem ( ServerRoom ) . ConfigureAwait ( false ) ;
2021-10-22 21:07:41 +08:00
}
2021-11-13 00:42:51 +08:00
private async Task duplicateCurrentItem ( )
2021-10-22 21:07:41 +08:00
{
2021-11-13 00:42:51 +08:00
Debug . Assert ( currentItem ! = null ) ;
2021-12-02 21:32:41 +08:00
await addItem ( new MultiplayerPlaylistItem
2021-11-13 00:42:51 +08:00
{
BeatmapID = currentItem . BeatmapID ,
2021-11-13 02:34:45 +08:00
BeatmapChecksum = currentItem . BeatmapChecksum ,
2021-11-13 00:42:51 +08:00
RulesetID = currentItem . RulesetID ,
2021-11-13 02:34:45 +08:00
RequiredMods = currentItem . RequiredMods ,
AllowedMods = currentItem . AllowedMods
2021-12-02 21:32:41 +08:00
} ) . ConfigureAwait ( false ) ;
}
2021-10-22 21:07:41 +08:00
2021-12-02 21:32:41 +08:00
private async Task addItem ( MultiplayerPlaylistItem item )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
Debug . Assert ( ServerAPIRoom ! = null ) ;
2021-12-02 21:32:41 +08:00
2021-12-03 15:48:54 +08:00
item . ID = + + lastPlaylistItemId ;
2021-12-06 14:09:06 +08:00
2022-07-01 17:58:22 +08:00
ServerRoom . Playlist . Add ( item ) ;
ServerAPIRoom . Playlist . Add ( new PlaylistItem ( item ) ) ;
2022-07-01 17:03:26 +08:00
await ( ( IMultiplayerClient ) this ) . PlaylistItemAdded ( clone ( item ) ) . ConfigureAwait ( false ) ;
2021-12-06 14:09:06 +08:00
2022-07-01 17:58:22 +08:00
await updatePlaylistOrder ( ServerRoom ) . ConfigureAwait ( false ) ;
2021-11-13 00:42:51 +08:00
}
2022-07-01 17:58:22 +08:00
private IEnumerable < MultiplayerPlaylistItem > upcomingItems = > ServerRoom ? . Playlist . Where ( i = > ! i . Expired ) . OrderBy ( i = > i . PlaylistOrder ) ? ? Enumerable . Empty < MultiplayerPlaylistItem > ( ) ;
2021-12-09 03:18:53 +08:00
2021-11-16 13:37:29 +08:00
private async Task updateCurrentItem ( MultiplayerRoom room , bool notify = true )
2021-11-13 00:42:51 +08:00
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
2021-12-04 21:34:37 +08:00
// Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item.
2022-07-01 17:58:22 +08:00
MultiplayerPlaylistItem nextItem = upcomingItems . FirstOrDefault ( ) ? ? ServerRoom . Playlist . OrderByDescending ( i = > i . PlayedAt ) . First ( ) ;
2021-12-03 19:05:25 +08:00
2022-07-01 17:58:22 +08:00
currentIndex = ServerRoom . Playlist . IndexOf ( nextItem ) ;
2021-12-02 21:32:41 +08:00
long lastItem = room . Settings . PlaylistItemId ;
room . Settings . PlaylistItemId = nextItem . ID ;
if ( notify & & nextItem . ID ! = lastItem )
2022-07-01 17:03:26 +08:00
await ( ( IMultiplayerClient ) this ) . SettingsChanged ( clone ( room . Settings ) ) . ConfigureAwait ( false ) ;
2021-12-02 21:32:41 +08:00
}
private async Task updatePlaylistOrder ( MultiplayerRoom room )
{
2022-07-01 17:58:22 +08:00
Debug . Assert ( ServerRoom ! = null ) ;
Debug . Assert ( ServerAPIRoom ! = null ) ;
2022-04-01 17:31:17 +08:00
2021-12-03 15:48:54 +08:00
List < MultiplayerPlaylistItem > orderedActiveItems ;
2021-11-13 00:42:51 +08:00
2021-11-16 13:37:29 +08:00
switch ( room . Settings . QueueMode )
2021-10-22 21:07:41 +08:00
{
2021-11-10 20:27:20 +08:00
default :
2022-07-01 17:58:22 +08:00
orderedActiveItems = ServerRoom . Playlist . Where ( item = > ! item . Expired ) . OrderBy ( item = > item . ID ) . ToList ( ) ;
2021-10-22 21:07:41 +08:00
break ;
2021-11-19 17:42:34 +08:00
case QueueMode . AllPlayersRoundRobin :
2021-12-06 23:01:07 +08:00
var itemsByPriority = new List < ( MultiplayerPlaylistItem item , int priority ) > ( ) ;
2021-12-03 15:48:54 +08:00
2021-12-06 23:01:07 +08:00
// Assign a priority for items from each user, starting from 0 and increasing in order which the user added the items.
2022-07-01 17:58:22 +08:00
foreach ( var group in ServerRoom . Playlist . Where ( item = > ! item . Expired ) . OrderBy ( item = > item . ID ) . GroupBy ( item = > item . OwnerID ) )
2021-12-02 21:32:41 +08:00
{
2021-12-06 23:01:07 +08:00
int priority = 0 ;
itemsByPriority . AddRange ( group . Select ( item = > ( item , priority + + ) ) ) ;
2021-12-02 21:32:41 +08:00
}
2021-12-06 23:01:07 +08:00
orderedActiveItems = itemsByPriority
// Order by each user's priority.
. OrderBy ( i = > i . priority )
// Many users will have the same priority of items, so attempt to break the tie by maintaining previous ordering.
// Suppose there are two users: User1 and User2. User1 adds two items, and then User2 adds a third. If the previous order is not maintained,
// then after playing the first item by User1, their second item will become priority=0 and jump to the front of the queue (because it was added first).
. ThenBy ( i = > i . item . PlaylistOrder )
// If there are still ties (normally shouldn't happen), break ties by making items added earlier go first.
// This could happen if e.g. the item orders get reset.
. ThenBy ( i = > i . item . ID )
. Select ( i = > i . item )
. ToList ( ) ;
2021-12-02 21:32:41 +08:00
break ;
2021-10-22 21:07:41 +08:00
}
2021-12-03 15:48:54 +08:00
for ( int i = 0 ; i < orderedActiveItems . Count ; i + + )
2021-12-02 21:32:41 +08:00
{
2021-12-03 19:05:25 +08:00
var item = orderedActiveItems [ i ] ;
if ( item . PlaylistOrder = = i )
continue ;
2021-11-13 00:42:51 +08:00
2021-12-03 19:05:25 +08:00
item . PlaylistOrder = ( ushort ) i ;
2021-11-16 13:37:29 +08:00
2022-07-01 17:03:26 +08:00
await ( ( IMultiplayerClient ) this ) . PlaylistItemChanged ( clone ( item ) ) . ConfigureAwait ( false ) ;
2021-12-02 21:32:41 +08:00
}
2022-04-01 17:31:17 +08:00
// Also ensure that the API room's playlist is correct.
2022-07-01 17:58:22 +08:00
foreach ( var item in ServerAPIRoom . Playlist )
item . PlaylistOrder = ServerRoom . Playlist . Single ( i = > i . ID = = item . ID ) . PlaylistOrder ;
2021-10-22 21:07:41 +08:00
}
2022-07-01 17:03:26 +08:00
private T clone < T > ( T incoming )
{
2023-09-16 13:31:33 +08:00
byte [ ] serialized = MessagePackSerializer . Serialize ( typeof ( T ) , incoming , SignalRUnionWorkaroundResolver . OPTIONS ) ;
2022-07-01 17:03:26 +08:00
return MessagePackSerializer . Deserialize < T > ( serialized , SignalRUnionWorkaroundResolver . OPTIONS ) ;
}
2023-05-25 19:09:40 +08:00
public static MultiplayerPlaylistItem CreateMultiplayerPlaylistItem ( PlaylistItem item ) = > new MultiplayerPlaylistItem
{
ID = item . ID ,
OwnerID = item . OwnerID ,
BeatmapID = item . Beatmap . OnlineID ,
BeatmapChecksum = item . Beatmap . MD5Hash ,
RulesetID = item . RulesetID ,
RequiredMods = item . RequiredMods . ToArray ( ) ,
AllowedMods = item . AllowedMods . ToArray ( ) ,
Expired = item . Expired ,
PlaylistOrder = item . PlaylistOrder ? ? 0 ,
PlayedAt = item . PlayedAt ,
StarRating = item . Beatmap . StarRating ,
} ;
2023-11-13 13:35:07 +08:00
public override Task DisconnectInternal ( )
{
isConnected . Value = false ;
return Task . CompletedTask ;
}
2020-12-19 00:14:50 +08:00
}
}