mirror of
https://github.com/ppy/osu.git
synced 2026-05-17 09:22:34 +08:00
51b4e89773
Client side requirements for making the client connect as soon as possible, based on how the client is being used. This is especially important with the introduction of ranked play: previously the worst case scenario would be that you couldn't join a multiplayer room (or spectate a user) and this was [automatically handled](https://github.com/ppy/osu/blob/f66e2c432fdb08db46477c4fa08ca74e551d037f/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs#L115-L121) mostly*, but now, if you leave the game open for a while, you can potentially be stuck queueing in ranked play with no users remaining on your server. Some samples of how this looks follow. Do note that the client is showing "Server is shutting down" errors. This is only going to happen in local debug environments – In production, when you reconnect to the endpoint you will always get a non-shutting-down instance. Idle scenario: https://github.com/user-attachments/assets/dd47fdf6-8d49-48e3-a19f-b196a581070b Non-idle scenario: https://github.com/user-attachments/assets/dfc8a41a-83fb-4b08-94b4-9595faf88294 * Spectator isn't handled properly, as one example.
1085 lines
44 KiB
C#
1085 lines
44 KiB
C#
// 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.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using MessagePack;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Extensions;
|
|
using osu.Framework.Utils;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Online;
|
|
using osu.Game.Online.API;
|
|
using osu.Game.Online.API.Requests.Responses;
|
|
using osu.Game.Online.Matchmaking;
|
|
using osu.Game.Online.Matchmaking.Events;
|
|
using osu.Game.Online.Matchmaking.Requests;
|
|
using osu.Game.Online.Matchmaking.Responses;
|
|
using osu.Game.Online.Multiplayer;
|
|
using osu.Game.Online.Multiplayer.Countdown;
|
|
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
|
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
|
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
|
using osu.Game.Online.RankedPlay;
|
|
using osu.Game.Online.Rooms;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Tests.Visual.OnlinePlay;
|
|
|
|
namespace osu.Game.Tests.Visual.Multiplayer
|
|
{
|
|
/// <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>
|
|
public partial class TestMultiplayerClient : MultiplayerClient
|
|
{
|
|
public override IBindable<bool> IsConnected => isConnected;
|
|
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
|
|
|
|
/// <summary>
|
|
/// The local client's <see cref="Room"/>. This is not always equivalent to the server-side room.
|
|
/// </summary>
|
|
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.");
|
|
|
|
public new MultiplayerRoomUser? LocalUser => ServerRoom?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
|
|
|
|
public Action<MultiplayerRoom>? RoomSetupAction;
|
|
|
|
public bool RoomJoined { get; private set; }
|
|
|
|
[Resolved]
|
|
private IAPIProvider api { get; set; } = null!;
|
|
|
|
private MultiplayerPlaylistItem? currentItem => ServerRoom?.Playlist[currentIndex];
|
|
private int currentIndex;
|
|
private long lastPlaylistItemId;
|
|
private int lastCountdownId;
|
|
|
|
private readonly Dictionary<int, long> matchmakingUserPicks = new Dictionary<int, long>();
|
|
|
|
private readonly TestRoomRequestsHandler apiRequestHandler;
|
|
|
|
public TestMultiplayerClient(TestRoomRequestsHandler? apiRequestHandler = null)
|
|
{
|
|
this.apiRequestHandler = apiRequestHandler ?? new TestRoomRequestsHandler();
|
|
}
|
|
|
|
public void Connect() => isConnected.Value = true;
|
|
|
|
public void Disconnect() => isConnected.Value = false;
|
|
|
|
public MultiplayerRoomUser AddUser(APIUser user, bool markAsPlaying = false)
|
|
=> AddUser(new MultiplayerRoomUser(user.Id) { User = user }, markAsPlaying);
|
|
|
|
public MultiplayerRoomUser AddUser(MultiplayerRoomUser roomUser, bool markAsPlaying = false)
|
|
{
|
|
addUser(roomUser);
|
|
|
|
if (markAsPlaying)
|
|
PlayingUserIds.Add(roomUser.UserID);
|
|
|
|
return roomUser;
|
|
}
|
|
|
|
public void TestAddUnresolvedUser() => addUser(new MultiplayerRoomUser(TestUserLookupCache.UNRESOLVED_USER_ID));
|
|
|
|
private void addUser(MultiplayerRoomUser user)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
ServerRoom.Users.Add(user);
|
|
((IMultiplayerClient)this).UserJoined(clone(user)).WaitSafely();
|
|
|
|
switch (ServerRoom?.MatchState)
|
|
{
|
|
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
|
|
.Select(team => (teamID: team.ID, userCount: ServerRoom.Users.Count(u => (u.MatchState as TeamVersusUserState)?.TeamID == team.ID)))
|
|
.MinBy(pair => pair.userCount).teamID;
|
|
|
|
user.MatchState = new TeamVersusUserState { TeamID = bestTeam };
|
|
((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).WaitSafely();
|
|
break;
|
|
|
|
case RankedPlayRoomState:
|
|
((RankedPlayRoomState)ServerRoom!.MatchState!).Users[user.UserID] = new RankedPlayUserInfo
|
|
{
|
|
Rating = 1500,
|
|
Hand = Enumerable.Range(0, 5).Select(_ => new RankedPlayCardItem()).ToList()
|
|
};
|
|
|
|
((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).WaitSafely();
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void RemoveUser(APIUser user)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
ServerRoom.Users.Remove(ServerRoom.Users.Single(u => u.UserID == user.Id));
|
|
((IMultiplayerClient)this).UserLeft(clone(new MultiplayerRoomUser(user.Id)));
|
|
|
|
if (ServerRoom.Users.Any())
|
|
TransferHost(ServerRoom.Users.First().UserID);
|
|
}
|
|
|
|
public void ChangeRoomState(MultiplayerRoomState newState)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
ServerRoom.State = clone(newState);
|
|
|
|
((IMultiplayerClient)this).RoomStateChanged(clone(ServerRoom.State));
|
|
}
|
|
|
|
public void ChangeUserState(int userId, MultiplayerUserState newState)
|
|
{
|
|
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));
|
|
|
|
updateRoomStateIfRequired();
|
|
}
|
|
|
|
private void updateRoomStateIfRequired()
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
switch (ServerRoom.State)
|
|
{
|
|
case MultiplayerRoomState.Open:
|
|
break;
|
|
|
|
case MultiplayerRoomState.WaitingForLoad:
|
|
if (ServerRoom.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
|
|
{
|
|
var loadedUsers = ServerRoom.Users.Where(u => u.State == MultiplayerUserState.Loaded).ToArray();
|
|
|
|
if (loadedUsers.Length == 0)
|
|
{
|
|
// all users have bailed from the load sequence. cancel the game start.
|
|
ChangeRoomState(MultiplayerRoomState.Open);
|
|
return;
|
|
}
|
|
|
|
foreach (var u in ServerRoom.Users.Where(u => u.State == MultiplayerUserState.Loaded))
|
|
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
|
|
|
|
((IMultiplayerClient)this).GameplayStarted();
|
|
|
|
ChangeRoomState(MultiplayerRoomState.Playing);
|
|
}
|
|
|
|
break;
|
|
|
|
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);
|
|
|
|
ChangeRoomState(MultiplayerRoomState.Open);
|
|
((IMultiplayerClient)this).ResultsReady();
|
|
|
|
FinishCurrentItem().WaitSafely();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void ChangeUserBeatmapAvailability(int userId, BeatmapAvailability newBeatmapAvailability)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
var user = ServerRoom.Users.Single(u => u.UserID == userId);
|
|
user.BeatmapAvailability = newBeatmapAvailability;
|
|
|
|
((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(clone(userId), clone(user.BeatmapAvailability));
|
|
}
|
|
|
|
protected override async Task<MultiplayerRoom> JoinRoomInternal(long roomId, string? password = null)
|
|
{
|
|
if (RoomJoined || ServerAPIRoom != null)
|
|
throw new InvalidOperationException("Already joined a room");
|
|
|
|
roomId = clone(roomId);
|
|
password = clone(password);
|
|
|
|
ServerAPIRoom = ServerSideRooms.Single(r => r.RoomID == roomId);
|
|
|
|
if (password != ServerAPIRoom.Password)
|
|
throw new InvalidOperationException("Invalid password.");
|
|
|
|
lastPlaylistItemId = ServerAPIRoom.Playlist.Max(item => item.ID);
|
|
|
|
var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
|
|
{
|
|
User = api.LocalUser.Value
|
|
};
|
|
|
|
ServerRoom = new MultiplayerRoom(roomId)
|
|
{
|
|
Settings =
|
|
{
|
|
Name = ServerAPIRoom.Name,
|
|
MatchType = ServerAPIRoom.Type,
|
|
Password = password ?? string.Empty,
|
|
QueueMode = ServerAPIRoom.QueueMode,
|
|
AutoStartDuration = ServerAPIRoom.AutoStartDuration
|
|
},
|
|
Playlist = ServerAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)).ToList(),
|
|
Users = { localUser },
|
|
Host = localUser,
|
|
ChannelID = ServerAPIRoom.ChannelId
|
|
};
|
|
|
|
await changeMatchType(ServerRoom.Settings.MatchType).ConfigureAwait(false);
|
|
await updatePlaylistOrder(ServerRoom).ConfigureAwait(false);
|
|
await updateCurrentItem(ServerRoom, false).ConfigureAwait(false);
|
|
|
|
RoomSetupAction?.Invoke(ServerRoom);
|
|
RoomSetupAction = null;
|
|
|
|
return clone(ServerRoom);
|
|
}
|
|
|
|
protected override void OnRoomJoined()
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
RoomJoined = true;
|
|
}
|
|
|
|
protected override Task LeaveRoomInternal()
|
|
{
|
|
RoomJoined = false;
|
|
ServerAPIRoom = null;
|
|
ServerRoom = null;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task InvitePlayer(int userId)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task TransferHost(int userId)
|
|
{
|
|
userId = clone(userId);
|
|
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
ServerRoom.Host = ServerRoom.Users.Single(u => u.UserID == userId);
|
|
|
|
return ((IMultiplayerClient)this).HostChanged(clone(userId));
|
|
}
|
|
|
|
public override Task KickUser(int userId)
|
|
{
|
|
userId = clone(userId);
|
|
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
var user = ServerRoom.Users.Single(u => u.UserID == userId);
|
|
ServerRoom.Users.Remove(user);
|
|
|
|
return ((IMultiplayerClient)this).UserKicked(clone(user));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simulates a change to the server-side room's settings without any other change.
|
|
/// </summary>
|
|
public async Task ChangeServerRoomSettings(MultiplayerRoomSettings settings)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
ServerRoom.Settings = settings;
|
|
|
|
await ((IMultiplayerClient)this).SettingsChanged(clone(settings)).ConfigureAwait(false);
|
|
}
|
|
|
|
public override async Task ChangeSettings(MultiplayerRoomSettings settings)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
// Server is authoritative for the time being.
|
|
settings = clone(settings);
|
|
settings.PlaylistItemId = ServerRoom.Settings.PlaylistItemId;
|
|
ServerRoom.Settings = settings;
|
|
|
|
await changeQueueMode(settings.QueueMode).ConfigureAwait(false);
|
|
|
|
await ((IMultiplayerClient)this).SettingsChanged(clone(settings)).ConfigureAwait(false);
|
|
|
|
foreach (var user in ServerRoom.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
|
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
|
|
|
|
await changeMatchType(settings.MatchType).ConfigureAwait(false);
|
|
updateRoomStateIfRequired();
|
|
}
|
|
|
|
public override Task ChangeState(MultiplayerUserState newState)
|
|
{
|
|
newState = clone(newState);
|
|
|
|
if (newState == MultiplayerUserState.Idle && LocalUser?.State == MultiplayerUserState.WaitingForLoad)
|
|
return Task.CompletedTask;
|
|
|
|
ChangeUserState(api.LocalUser.Value.Id, clone(newState));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability)
|
|
{
|
|
ChangeUserBeatmapAvailability(api.LocalUser.Value.Id, clone(newBeatmapAvailability));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task ChangeUserStyle(int? beatmapId, int? rulesetId)
|
|
{
|
|
ChangeUserStyle(api.LocalUser.Value.Id, beatmapId, rulesetId);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public void ChangeUserStyle(int userId, int? beatmapId, int? rulesetId)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
var user = ServerRoom.Users.Single(u => u.UserID == userId);
|
|
user.BeatmapId = beatmapId;
|
|
user.RulesetId = rulesetId;
|
|
|
|
((IMultiplayerClient)this).UserStyleChanged(userId, beatmapId, rulesetId);
|
|
}
|
|
|
|
public void ChangeUserMods(int userId, IEnumerable<Mod> newMods)
|
|
=> ChangeUserMods(userId, newMods.Select(m => new APIMod(m)));
|
|
|
|
public void ChangeUserMods(int userId, IEnumerable<APIMod> newMods)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
var user = ServerRoom.Users.Single(u => u.UserID == userId);
|
|
user.Mods = newMods.ToArray();
|
|
|
|
((IMultiplayerClient)this).UserModsChanged(clone(userId), clone(user.Mods));
|
|
}
|
|
|
|
public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
|
|
{
|
|
ChangeUserMods(api.LocalUser.Value.Id, clone(newMods));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task SendMatchRequest(MatchUserRequest request)
|
|
=> SendUserMatchRequest(api.LocalUser.Value.OnlineID, request);
|
|
|
|
public async Task SendUserMatchRequest(int userId, MatchUserRequest request)
|
|
{
|
|
request = clone(request);
|
|
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(LocalUser != null);
|
|
|
|
switch (request)
|
|
{
|
|
case ChangeTeamRequest changeTeam:
|
|
TeamVersusRoomState roomState = (TeamVersusRoomState)ServerRoom.MatchState!;
|
|
TeamVersusUserState userState = (TeamVersusUserState)LocalUser.MatchState!;
|
|
|
|
var targetTeam = roomState.Teams.FirstOrDefault(t => t.ID == changeTeam.TeamID);
|
|
|
|
if (targetTeam != null)
|
|
{
|
|
userState.TeamID = targetTeam.ID;
|
|
await ((IMultiplayerClient)this).MatchUserStateChanged(userId, clone(userState)).ConfigureAwait(false);
|
|
}
|
|
|
|
break;
|
|
|
|
case StartMatchCountdownRequest startCountdown:
|
|
await StartCountdown(new MatchStartCountdown { TimeRemaining = startCountdown.Duration }).ConfigureAwait(false);
|
|
break;
|
|
|
|
case StopCountdownRequest stopCountdown:
|
|
await StopCountdown(ServerRoom.ActiveCountdowns.First(c => c.ID == stopCountdown.ID)).ConfigureAwait(false);
|
|
break;
|
|
|
|
case RollRequest rollRequest:
|
|
int max = (int)(rollRequest.Max ?? 100);
|
|
await ((IMultiplayerClient)this).MatchEvent(new RollEvent
|
|
{
|
|
UserID = userId,
|
|
Max = (uint)max,
|
|
Result = (uint)RNG.Next(1, max + 1)
|
|
}).ConfigureAwait(false);
|
|
break;
|
|
|
|
case MatchmakingAvatarActionRequest avatarAction:
|
|
await ((IMultiplayerClient)this).MatchEvent(new MatchmakingAvatarActionEvent
|
|
{
|
|
UserId = userId,
|
|
Action = avatarAction.Action
|
|
}).ConfigureAwait(false);
|
|
break;
|
|
|
|
case RankedPlayCardHandReplayRequest cardHandState:
|
|
await ((IMultiplayerClient)this).MatchEvent(new RankedPlayCardHandReplayEvent
|
|
{
|
|
UserId = userId,
|
|
Frames = cardHandState.Frames,
|
|
}).ConfigureAwait(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public async Task StartCountdown(MultiplayerCountdown countdown)
|
|
{
|
|
countdown.ID = ++lastCountdownId;
|
|
countdown = clone(countdown);
|
|
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(LocalUser != null);
|
|
|
|
if (countdown.IsExclusive)
|
|
{
|
|
MultiplayerCountdown? existingCountdown = ServerRoom.ActiveCountdowns.FirstOrDefault(c => c.GetType() == countdown.GetType());
|
|
if (existingCountdown != null)
|
|
await StopCountdown(existingCountdown).ConfigureAwait(false);
|
|
}
|
|
|
|
ServerRoom.ActiveCountdowns.Add(countdown);
|
|
await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStartedEvent(ServerRoom.ActiveCountdowns[^1]))).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task StopCountdown(MultiplayerCountdown countdown)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(LocalUser != null);
|
|
|
|
ServerRoom.ActiveCountdowns.Remove(ServerRoom.ActiveCountdowns.First(c => c.ID == countdown.ID));
|
|
await ((IMultiplayerClient)this).MatchEvent(clone(new CountdownStoppedEvent(countdown.ID))).ConfigureAwait(false);
|
|
}
|
|
|
|
public override Task StartMatch()
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
ChangeRoomState(MultiplayerRoomState.WaitingForLoad);
|
|
foreach (var user in ServerRoom.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
|
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
|
|
|
|
return ((IMultiplayerClient)this).LoadRequested();
|
|
}
|
|
|
|
public override Task AbortGameplay()
|
|
{
|
|
Debug.Assert(LocalUser != null);
|
|
|
|
ChangeUserState(LocalUser.UserID, MultiplayerUserState.Idle);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override async Task AbortMatch()
|
|
{
|
|
ChangeUserState(api.LocalUser.Value.Id, MultiplayerUserState.Idle);
|
|
await ((IMultiplayerClient)this).GameplayAborted(GameplayAbortReason.HostAbortedTheMatch).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(currentItem != null);
|
|
|
|
if (ServerRoom.Settings.QueueMode == QueueMode.HostOnly && ServerRoom.Host?.UserID != LocalUser?.UserID)
|
|
throw new InvalidOperationException("Local user is not the room host.");
|
|
|
|
item.OwnerID = userId;
|
|
|
|
await addItem(item).ConfigureAwait(false);
|
|
await updateCurrentItem(ServerRoom).ConfigureAwait(false);
|
|
updateRoomStateIfRequired();
|
|
}
|
|
|
|
public override Task AddPlaylistItem(MultiplayerPlaylistItem item) => AddUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(item));
|
|
|
|
public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(currentItem != null);
|
|
Debug.Assert(ServerAPIRoom != null);
|
|
|
|
item.OwnerID = userId;
|
|
|
|
var existingItem = ServerRoom.Playlist.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 && ServerRoom.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;
|
|
|
|
ServerRoom.Playlist[ServerRoom.Playlist.IndexOf(existingItem)] = item;
|
|
ServerAPIRoom.Playlist = ServerAPIRoom.Playlist.Select((pi, i) => pi.ID == item.ID ? new PlaylistItem(item) : ServerAPIRoom.Playlist[i]).ToArray();
|
|
|
|
await ((IMultiplayerClient)this).PlaylistItemChanged(clone(item)).ConfigureAwait(false);
|
|
}
|
|
|
|
public override Task EditPlaylistItem(MultiplayerPlaylistItem item) => EditUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(item));
|
|
|
|
public async Task RemoveUserPlaylistItem(int userId, long playlistItemId)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(ServerAPIRoom != null);
|
|
|
|
var item = ServerRoom.Playlist.FirstOrDefault(i => i.ID == playlistItemId);
|
|
|
|
if (item == null)
|
|
throw new InvalidOperationException("Item does not exist in the room.");
|
|
|
|
if (item.Equals(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.");
|
|
|
|
if (item.Expired)
|
|
throw new InvalidOperationException("Attempted to remove an item which has already been played.");
|
|
|
|
ServerRoom.Playlist.Remove(item);
|
|
ServerAPIRoom.Playlist = ServerAPIRoom.Playlist.Where(i => i.ID != item.ID).ToArray();
|
|
await ((IMultiplayerClient)this).PlaylistItemRemoved(clone(playlistItemId)).ConfigureAwait(false);
|
|
|
|
await updateCurrentItem(ServerRoom).ConfigureAwait(false);
|
|
updateRoomStateIfRequired();
|
|
}
|
|
|
|
public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId));
|
|
|
|
public override Task VoteToSkipIntro()
|
|
{
|
|
return UserVoteToSkipIntro(api.LocalUser.Value.OnlineID);
|
|
}
|
|
|
|
public async Task UserVoteToSkipIntro(int userId)
|
|
{
|
|
await ((IMultiplayerClient)this).UserVotedToSkipIntro(userId, true).ConfigureAwait(false);
|
|
}
|
|
|
|
protected override Task<MultiplayerRoom> CreateRoomInternal(MultiplayerRoom room)
|
|
{
|
|
Room apiRoom = new Room(room)
|
|
{
|
|
Type = room.Settings.MatchType == MatchType.Playlists
|
|
? MatchType.HeadToHead
|
|
: room.Settings.MatchType
|
|
};
|
|
|
|
AddServerSideRoom(apiRoom, api.LocalUser.Value);
|
|
return JoinRoomInternal(apiRoom.RoomID!.Value, room.Settings.Password);
|
|
}
|
|
|
|
private async Task changeMatchType(MatchType type)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
switch (type)
|
|
{
|
|
case MatchType.HeadToHead:
|
|
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);
|
|
}
|
|
|
|
break;
|
|
|
|
case MatchType.TeamVersus:
|
|
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);
|
|
}
|
|
|
|
break;
|
|
|
|
case MatchType.Matchmaking:
|
|
ServerRoom.MatchState = new MatchmakingRoomState();
|
|
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);
|
|
}
|
|
|
|
break;
|
|
|
|
case MatchType.RankedPlay:
|
|
ServerRoom.MatchState = new RankedPlayRoomState();
|
|
|
|
foreach (var user in ServerRoom.Users)
|
|
{
|
|
((RankedPlayRoomState)ServerRoom.MatchState).Users[user.UserID] = new RankedPlayUserInfo
|
|
{
|
|
Rating = 1500,
|
|
Hand = Enumerable.Range(0, 5).Select(_ => new RankedPlayCardItem()).ToList()
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
private async Task changeQueueMode(QueueMode newMode)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(currentItem != null);
|
|
|
|
// When changing to host-only mode, ensure that at least one non-expired playlist item exists by duplicating the current item.
|
|
if (newMode == QueueMode.HostOnly && ServerRoom.Playlist.All(item => item.Expired))
|
|
await duplicateCurrentItem().ConfigureAwait(false);
|
|
|
|
await updatePlaylistOrder(ServerRoom).ConfigureAwait(false);
|
|
await updateCurrentItem(ServerRoom).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task FinishCurrentItem()
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(currentItem != null);
|
|
|
|
// Expire the current playlist item.
|
|
currentItem.Expired = true;
|
|
currentItem.PlayedAt = DateTimeOffset.Now;
|
|
|
|
await ((IMultiplayerClient)this).PlaylistItemChanged(clone(currentItem)).ConfigureAwait(false);
|
|
await updatePlaylistOrder(ServerRoom).ConfigureAwait(false);
|
|
|
|
// In host-only mode, a duplicate playlist item will be used for the next round.
|
|
if (ServerRoom.Settings.QueueMode == QueueMode.HostOnly && ServerRoom.Playlist.All(item => item.Expired))
|
|
await duplicateCurrentItem().ConfigureAwait(false);
|
|
|
|
await updateCurrentItem(ServerRoom).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task duplicateCurrentItem()
|
|
{
|
|
Debug.Assert(currentItem != null);
|
|
|
|
await addItem(new MultiplayerPlaylistItem
|
|
{
|
|
BeatmapID = currentItem.BeatmapID,
|
|
BeatmapChecksum = currentItem.BeatmapChecksum,
|
|
RulesetID = currentItem.RulesetID,
|
|
RequiredMods = currentItem.RequiredMods,
|
|
AllowedMods = currentItem.AllowedMods
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task addItem(MultiplayerPlaylistItem item)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(ServerAPIRoom != null);
|
|
|
|
item.ID = ++lastPlaylistItemId;
|
|
|
|
ServerRoom.Playlist.Add(item);
|
|
ServerAPIRoom.Playlist = ServerAPIRoom.Playlist.Append(new PlaylistItem(item)).ToArray();
|
|
await ((IMultiplayerClient)this).PlaylistItemAdded(clone(item)).ConfigureAwait(false);
|
|
|
|
await updatePlaylistOrder(ServerRoom).ConfigureAwait(false);
|
|
}
|
|
|
|
private IEnumerable<MultiplayerPlaylistItem> upcomingItems => ServerRoom?.Playlist.Where(i => !i.Expired).OrderBy(i => i.PlaylistOrder) ?? Enumerable.Empty<MultiplayerPlaylistItem>();
|
|
|
|
private async Task updateCurrentItem(MultiplayerRoom room, bool notify = true)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
// Pick the next non-expired playlist item by playlist order, or default to the most-recently-expired item.
|
|
MultiplayerPlaylistItem nextItem = upcomingItems.FirstOrDefault() ?? ServerRoom.Playlist.OrderByDescending(i => i.PlayedAt).First();
|
|
|
|
currentIndex = ServerRoom.Playlist.IndexOf(nextItem);
|
|
|
|
long lastItem = room.Settings.PlaylistItemId;
|
|
room.Settings.PlaylistItemId = nextItem.ID;
|
|
|
|
if (notify && nextItem.ID != lastItem)
|
|
await ((IMultiplayerClient)this).SettingsChanged(clone(room.Settings)).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task updatePlaylistOrder(MultiplayerRoom room)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
Debug.Assert(ServerAPIRoom != null);
|
|
|
|
List<MultiplayerPlaylistItem> orderedActiveItems;
|
|
|
|
switch (room.Settings.QueueMode)
|
|
{
|
|
default:
|
|
orderedActiveItems = ServerRoom.Playlist.Where(item => !item.Expired).OrderBy(item => item.ID).ToList();
|
|
break;
|
|
|
|
case QueueMode.AllPlayersRoundRobin:
|
|
var itemsByPriority = new List<(MultiplayerPlaylistItem item, int priority)>();
|
|
|
|
// Assign a priority for items from each user, starting from 0 and increasing in order which the user added the items.
|
|
foreach (var group in ServerRoom.Playlist.Where(item => !item.Expired).OrderBy(item => item.ID).GroupBy(item => item.OwnerID))
|
|
{
|
|
int priority = 0;
|
|
itemsByPriority.AddRange(group.Select(item => (item, priority++)));
|
|
}
|
|
|
|
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();
|
|
|
|
break;
|
|
}
|
|
|
|
for (int i = 0; i < orderedActiveItems.Count; i++)
|
|
{
|
|
var item = orderedActiveItems[i];
|
|
|
|
if (item.PlaylistOrder == i)
|
|
continue;
|
|
|
|
item.PlaylistOrder = (ushort)i;
|
|
|
|
await ((IMultiplayerClient)this).PlaylistItemChanged(clone(item)).ConfigureAwait(false);
|
|
}
|
|
|
|
// Also ensure that the API room's playlist is correct.
|
|
foreach (var item in ServerAPIRoom.Playlist)
|
|
item.PlaylistOrder = ServerRoom.Playlist.Single(i => i.ID == item.ID).PlaylistOrder;
|
|
}
|
|
|
|
private T clone<T>(T incoming)
|
|
{
|
|
byte[] serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS);
|
|
return MessagePackSerializer.Deserialize<T>(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
|
|
}
|
|
|
|
protected override Task DisconnectInternal()
|
|
{
|
|
Disconnect();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task Reconnect()
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task ChangeMatchRoomState(MatchRoomState state)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
ServerRoom.MatchState = state;
|
|
await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom.MatchState)).ConfigureAwait(false);
|
|
}
|
|
|
|
public override Task DiscardCards(RankedPlayCardItem[] cards)
|
|
=> DiscardCards(_ => cards);
|
|
|
|
public Task DiscardCards(Func<RankedPlayCardItem[], IEnumerable<RankedPlayCardItem>> selector)
|
|
=> DiscardUserCards(api.LocalUser.Value.OnlineID, selector);
|
|
|
|
public async Task DiscardUserCards(int userId, Func<RankedPlayCardItem[], IEnumerable<RankedPlayCardItem>> selector)
|
|
{
|
|
RankedPlayUserInfo info = ((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId];
|
|
RankedPlayCardItem[] cards = selector(info.Hand.ToArray()).ToArray();
|
|
|
|
await RankedPlayRemoveUserCards(userId, _ => cards).ConfigureAwait(false);
|
|
await RankedPlayAddUserCards(userId, Enumerable.Range(0, cards.Length).Select(_ => new RankedPlayCardItem()).ToArray()).ConfigureAwait(false);
|
|
}
|
|
|
|
public override Task PlayCard(RankedPlayCardItem card)
|
|
=> PlayCard(_ => card);
|
|
|
|
public Task PlayCard(Func<RankedPlayCardItem[], RankedPlayCardItem> selector)
|
|
=> PlayUserCard(api.LocalUser.Value.OnlineID, selector);
|
|
|
|
public async Task PlayUserCard(int userId, Func<RankedPlayCardItem[], RankedPlayCardItem> selector)
|
|
{
|
|
RankedPlayCardItem card = selector(((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId].Hand.ToArray());
|
|
MultiplayerPlaylistItem? item = GetCardWithPlaylistItem(card).PlaylistItem.Value;
|
|
|
|
if (item != null)
|
|
{
|
|
ServerRoom!.Playlist.Add(item);
|
|
await ((IMultiplayerClient)this).PlaylistItemAdded(clone(item)).ConfigureAwait(false);
|
|
await ((IMultiplayerClient)this).PlaylistItemChanged(clone(item)).ConfigureAwait(false);
|
|
|
|
var settings = clone(ServerRoom!.Settings);
|
|
settings.PlaylistItemId = item.ID;
|
|
await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false);
|
|
}
|
|
|
|
await ((IRankedPlayClient)this).RankedPlayCardPlayed(clone(card)).ConfigureAwait(false);
|
|
}
|
|
|
|
public override Task<MatchmakingPool[]> GetMatchmakingPoolsOfType(MatchmakingPoolType type)
|
|
{
|
|
return Task.FromResult<MatchmakingPool[]>(
|
|
[
|
|
new MatchmakingPool { Id = 0, RulesetId = 0 },
|
|
new MatchmakingPool { Id = 1, RulesetId = 1 },
|
|
new MatchmakingPool { Id = 2, RulesetId = 2 },
|
|
new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4 },
|
|
new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7 },
|
|
]);
|
|
}
|
|
|
|
public override Task<MatchmakingJoinLobbyResponse> MatchmakingJoinLobbyWithParams(MatchmakingJoinLobbyRequest request)
|
|
{
|
|
return Task.FromResult(new MatchmakingJoinLobbyResponse());
|
|
}
|
|
|
|
public override Task MatchmakingLeaveLobby()
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override async Task MatchmakingJoinQueue(int poolId)
|
|
{
|
|
await ((IMatchmakingClient)this).MatchmakingQueueJoined().ConfigureAwait(false);
|
|
await ((IMatchmakingClient)this).MatchmakingQueueStatusChanged(new MatchmakingQueueStatus.Searching()).ConfigureAwait(false);
|
|
}
|
|
|
|
public override async Task MatchmakingLeaveQueue()
|
|
{
|
|
await ((IMatchmakingClient)this).MatchmakingQueueLeft().ConfigureAwait(false);
|
|
}
|
|
|
|
public override Task MatchmakingAcceptInvitation()
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override Task<MatchmakingIssueDuelResponse> MatchmakingIssueDuel(MatchmakingIssueDuelRequest request)
|
|
{
|
|
return Task.FromResult(new MatchmakingIssueDuelResponse());
|
|
}
|
|
|
|
public override Task<MatchmakingAcceptDuelResponse> MatchmakingAcceptDuel(MatchmakingAcceptDuelRequest request)
|
|
{
|
|
return Task.FromResult(new MatchmakingAcceptDuelResponse());
|
|
}
|
|
|
|
public override Task MatchmakingDeclineInvitation()
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public new async Task MatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status)
|
|
{
|
|
await ((IMatchmakingClient)this).MatchmakingLobbyStatusChanged(clone(status)).ConfigureAwait(false);
|
|
}
|
|
|
|
public override Task MatchmakingToggleSelection(long playlistItemId)
|
|
=> MatchmakingToggleUserSelection(api.LocalUser.Value.OnlineID, playlistItemId);
|
|
|
|
public override Task MatchmakingSkipToNextStage()
|
|
=> Task.CompletedTask;
|
|
|
|
public async Task MatchmakingToggleUserSelection(int userId, long playlistItemId)
|
|
{
|
|
if (matchmakingUserPicks.TryGetValue(userId, out long existingId))
|
|
{
|
|
if (existingId == playlistItemId)
|
|
return;
|
|
|
|
await ((IMatchmakingClient)this).MatchmakingItemDeselected(clone(userId), clone(existingId)).ConfigureAwait(false);
|
|
}
|
|
|
|
matchmakingUserPicks[userId] = playlistItemId;
|
|
|
|
await ((IMatchmakingClient)this).MatchmakingItemSelected(clone(userId), clone(playlistItemId)).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task MatchmakingChangeStage(MatchmakingStage stage, Action<MatchmakingRoomState>? prepare = null)
|
|
{
|
|
MatchmakingRoomState state = clone((MatchmakingRoomState)ServerRoom!.MatchState!);
|
|
|
|
state.Stage = stage;
|
|
|
|
if (stage == MatchmakingStage.RoundWarmupTime)
|
|
state.CurrentRound++;
|
|
|
|
prepare?.Invoke(state);
|
|
|
|
await ChangeMatchRoomState(state).ConfigureAwait(false);
|
|
await StartCountdown(new MatchmakingStageCountdown
|
|
{
|
|
Stage = stage,
|
|
TimeRemaining = TimeSpan.FromSeconds(stage == MatchmakingStage.UserBeatmapSelect ? 30 : 10)
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a card to the local user's hand.
|
|
/// </summary>
|
|
public Task RankedPlayAddCards(RankedPlayCardItem[] cards)
|
|
=> RankedPlayAddUserCards(api.LocalUser.Value.OnlineID, cards);
|
|
|
|
/// <summary>
|
|
/// Adds a card to the given user's hand.
|
|
/// </summary>
|
|
public async Task RankedPlayAddUserCards(int userId, RankedPlayCardItem[] cards)
|
|
{
|
|
foreach (var card in cards)
|
|
{
|
|
((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId].Hand.Add(card);
|
|
await ((IRankedPlayClient)this).RankedPlayCardAdded(userId, clone(card)).ConfigureAwait(false);
|
|
}
|
|
|
|
await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom!.MatchState)).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a card from the local user's hand.
|
|
/// </summary>
|
|
public Task RankedPlayRemoveCards(Func<RankedPlayCardItem[], RankedPlayCardItem[]> selector)
|
|
=> RankedPlayRemoveUserCards(api.LocalUser.Value.OnlineID, selector);
|
|
|
|
/// <summary>
|
|
/// Removes a card from the given user's hand.
|
|
/// </summary>
|
|
public async Task RankedPlayRemoveUserCards(int userId, Func<RankedPlayCardItem[], RankedPlayCardItem[]> selector)
|
|
{
|
|
RankedPlayCardItem[] cards = selector(((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId].Hand.ToArray());
|
|
|
|
foreach (var card in cards)
|
|
{
|
|
((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId].Hand.Remove(card);
|
|
await ((IRankedPlayClient)this).RankedPlayCardRemoved(userId, clone(card)).ConfigureAwait(false);
|
|
}
|
|
|
|
await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom!.MatchState)).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reveals a card in the local user's hand.
|
|
/// </summary>
|
|
public Task RankedPlayRevealCard(Func<RankedPlayCardItem[], RankedPlayCardItem> selector, MultiplayerPlaylistItem item)
|
|
=> RankedPlayRevealUserCard(api.LocalUser.Value.OnlineID, selector, item);
|
|
|
|
/// <summary>
|
|
/// Reveals a card in the given user's hand.
|
|
/// </summary>
|
|
public async Task RankedPlayRevealUserCard(int userId, Func<RankedPlayCardItem[], RankedPlayCardItem> selector, MultiplayerPlaylistItem item)
|
|
{
|
|
RankedPlayCardItem card = selector(((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId].Hand.ToArray());
|
|
await ((IRankedPlayClient)this).RankedPlayCardRevealed(clone(card), clone(item)).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task RankedPlayChangeStage(RankedPlayStage stage, Action<RankedPlayRoomState>? prepare = null)
|
|
{
|
|
RankedPlayRoomState state = clone((RankedPlayRoomState)ServerRoom!.MatchState!);
|
|
|
|
state.Stage = stage;
|
|
|
|
if (stage == RankedPlayStage.RoundWarmup)
|
|
state.CurrentRound++;
|
|
|
|
prepare?.Invoke(state);
|
|
|
|
await ChangeMatchRoomState(state).ConfigureAwait(false);
|
|
await StartCountdown(new RankedPlayStageCountdown
|
|
{
|
|
Stage = stage,
|
|
TimeRemaining = TimeSpan.FromSeconds(stage == RankedPlayStage.CardPlay ? 30 : 10)
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task RankedPlayChangeUserState(int userId, Action<RankedPlayUserInfo> prepare)
|
|
{
|
|
Debug.Assert(ServerRoom != null);
|
|
|
|
var userInfo = clone(((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId]);
|
|
prepare(userInfo);
|
|
|
|
((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId] = userInfo;
|
|
await ((IMultiplayerClient)this).MatchRoomStateChanged(clone(ServerRoom!.MatchState)).ConfigureAwait(false);
|
|
}
|
|
|
|
#region API Room Handling
|
|
|
|
public IReadOnlyList<Room> ServerSideRooms
|
|
=> apiRequestHandler.ServerSideRooms;
|
|
|
|
public void AddServerSideRoom(Room room, APIUser host)
|
|
=> apiRequestHandler.AddServerSideRoom(room, host);
|
|
|
|
public bool HandleRequest(APIRequest request, APIUser localUser, BeatmapManager beatmapManager)
|
|
=> apiRequestHandler.HandleRequest(request, localUser, beatmapManager);
|
|
|
|
#endregion
|
|
}
|
|
}
|