mirror of
https://github.com/osukey/osukey.git
synced 2025-06-09 05:19:11 +09:00
Merge pull request #11205 from smoogipoo/stateful-multiplayer-client
Implement a stateful multiplayer client + test realtime room manager
This commit is contained in:
commit
afabc4712e
@ -0,0 +1,153 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
[HeadlessTest]
|
||||||
|
public class TestSceneRealtimeRoomManager : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
private TestRealtimeRoomContainer roomContainer;
|
||||||
|
private TestRealtimeRoomManager roomManager => roomContainer.RoomManager;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPollsInitially()
|
||||||
|
{
|
||||||
|
AddStep("create room manager with a few rooms", () =>
|
||||||
|
{
|
||||||
|
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
roomManager.CreateRoom(new Room { Name = { Value = "1" } });
|
||||||
|
roomManager.PartRoom();
|
||||||
|
roomManager.CreateRoom(new Room { Name = { Value = "2" } });
|
||||||
|
roomManager.PartRoom();
|
||||||
|
roomManager.ClearRooms();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
|
||||||
|
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRoomsClearedOnDisconnection()
|
||||||
|
{
|
||||||
|
AddStep("create room manager with a few rooms", () =>
|
||||||
|
{
|
||||||
|
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
roomManager.CreateRoom(new Room());
|
||||||
|
roomManager.PartRoom();
|
||||||
|
roomManager.CreateRoom(new Room());
|
||||||
|
roomManager.PartRoom();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("disconnect", () => roomContainer.Client.Disconnect());
|
||||||
|
|
||||||
|
AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0);
|
||||||
|
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRoomsPolledOnReconnect()
|
||||||
|
{
|
||||||
|
AddStep("create room manager with a few rooms", () =>
|
||||||
|
{
|
||||||
|
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
roomManager.CreateRoom(new Room());
|
||||||
|
roomManager.PartRoom();
|
||||||
|
roomManager.CreateRoom(new Room());
|
||||||
|
roomManager.PartRoom();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("disconnect", () => roomContainer.Client.Disconnect());
|
||||||
|
AddStep("connect", () => roomContainer.Client.Connect());
|
||||||
|
|
||||||
|
AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
|
||||||
|
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRoomsNotPolledWhenJoined()
|
||||||
|
{
|
||||||
|
AddStep("create room manager with a room", () =>
|
||||||
|
{
|
||||||
|
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
roomManager.CreateRoom(new Room());
|
||||||
|
roomManager.ClearRooms();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0);
|
||||||
|
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiplayerRoomJoinedWhenCreated()
|
||||||
|
{
|
||||||
|
AddStep("create room manager with a room", () =>
|
||||||
|
{
|
||||||
|
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
roomManager.CreateRoom(new Room());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiplayerRoomPartedWhenAPIRoomParted()
|
||||||
|
{
|
||||||
|
AddStep("create room manager with a room", () =>
|
||||||
|
{
|
||||||
|
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
roomManager.CreateRoom(new Room());
|
||||||
|
roomManager.PartRoom();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiplayerRoomJoinedWhenAPIRoomJoined()
|
||||||
|
{
|
||||||
|
AddStep("create room manager with a room", () =>
|
||||||
|
{
|
||||||
|
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||||
|
{
|
||||||
|
var r = new Room();
|
||||||
|
roomManager.CreateRoom(r);
|
||||||
|
roomManager.PartRoom();
|
||||||
|
roomManager.JoinRoom(r);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestRealtimeRoomManager createRoomManager()
|
||||||
|
{
|
||||||
|
Child = roomContainer = new TestRealtimeRoomContainer
|
||||||
|
{
|
||||||
|
RoomManager =
|
||||||
|
{
|
||||||
|
TimeBetweenListingPolls = { Value = 1 },
|
||||||
|
TimeBetweenSelectionPolls = { Value = 1 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return roomManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,16 +7,16 @@ namespace osu.Game.Online.API.Requests
|
|||||||
{
|
{
|
||||||
public class GetBeatmapSetRequest : APIRequest<APIBeatmapSet>
|
public class GetBeatmapSetRequest : APIRequest<APIBeatmapSet>
|
||||||
{
|
{
|
||||||
private readonly int id;
|
public readonly int ID;
|
||||||
private readonly BeatmapSetLookupType type;
|
public readonly BeatmapSetLookupType Type;
|
||||||
|
|
||||||
public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId)
|
public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId)
|
||||||
{
|
{
|
||||||
this.id = id;
|
ID = id;
|
||||||
this.type = type;
|
Type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Target => type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{id}" : $@"beatmapsets/lookup?beatmap_id={id}";
|
protected override string Target => Type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{ID}" : $@"beatmapsets/lookup?beatmap_id={ID}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum BeatmapSetLookupType
|
public enum BeatmapSetLookupType
|
||||||
|
@ -10,11 +10,11 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
{
|
{
|
||||||
public class CreateRoomRequest : APIRequest<APICreatedRoom>
|
public class CreateRoomRequest : APIRequest<APICreatedRoom>
|
||||||
{
|
{
|
||||||
private readonly Room room;
|
public readonly Room Room;
|
||||||
|
|
||||||
public CreateRoomRequest(Room room)
|
public CreateRoomRequest(Room room)
|
||||||
{
|
{
|
||||||
this.room = room;
|
Room = room;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override WebRequest CreateWebRequest()
|
protected override WebRequest CreateWebRequest()
|
||||||
@ -24,7 +24,7 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
req.ContentType = "application/json";
|
req.ContentType = "application/json";
|
||||||
req.Method = HttpMethod.Post;
|
req.Method = HttpMethod.Post;
|
||||||
|
|
||||||
req.AddRaw(JsonConvert.SerializeObject(room));
|
req.AddRaw(JsonConvert.SerializeObject(Room));
|
||||||
|
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,13 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
{
|
{
|
||||||
public class GetRoomRequest : APIRequest<Room>
|
public class GetRoomRequest : APIRequest<Room>
|
||||||
{
|
{
|
||||||
private readonly int roomId;
|
public readonly int RoomId;
|
||||||
|
|
||||||
public GetRoomRequest(int roomId)
|
public GetRoomRequest(int roomId)
|
||||||
{
|
{
|
||||||
this.roomId = roomId;
|
RoomId = roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Target => $"rooms/{roomId}";
|
protected override string Target => $"rooms/{RoomId}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
385
osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs
Normal file
385
osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Multiplayer.RoomStatuses;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when any change occurs to the multiplayer room.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? RoomChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? LoadRequested;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the multiplayer server requests gameplay to be started.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? MatchStarted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the multiplayer server has finished collating results.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? ResultsReady;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the <see cref="StatefulMultiplayerClient"/> is currently connected.
|
||||||
|
/// </summary>
|
||||||
|
public abstract IBindable<bool> IsConnected { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The joined <see cref="MultiplayerRoom"/>.
|
||||||
|
/// </summary>
|
||||||
|
public MultiplayerRoom? Room { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The users currently in gameplay.
|
||||||
|
/// </summary>
|
||||||
|
public readonly BindableList<int> PlayingUsers = new BindableList<int>();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RulesetStore rulesets { get; set; } = null!;
|
||||||
|
|
||||||
|
private Room? apiRoom;
|
||||||
|
|
||||||
|
// Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise.
|
||||||
|
private int playlistItemId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Joins the <see cref="MultiplayerRoom"/> for a given API <see cref="Room"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="room">The API <see cref="Room"/>.</param>
|
||||||
|
public async Task JoinRoom(Room room)
|
||||||
|
{
|
||||||
|
Debug.Assert(Room == null);
|
||||||
|
Debug.Assert(room.RoomID.Value != null);
|
||||||
|
|
||||||
|
apiRoom = room;
|
||||||
|
playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0;
|
||||||
|
|
||||||
|
Room = await JoinRoom(room.RoomID.Value.Value);
|
||||||
|
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
foreach (var user in Room.Users)
|
||||||
|
await PopulateUser(user);
|
||||||
|
|
||||||
|
updateLocalRoomSettings(Room.Settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Joins the <see cref="MultiplayerRoom"/> with a given ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="roomId">The room ID.</param>
|
||||||
|
/// <returns>The joined <see cref="MultiplayerRoom"/>.</returns>
|
||||||
|
protected abstract Task<MultiplayerRoom> JoinRoom(long roomId);
|
||||||
|
|
||||||
|
public virtual Task LeaveRoom()
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
apiRoom = null;
|
||||||
|
Room = null;
|
||||||
|
|
||||||
|
Schedule(() => RoomChanged?.Invoke());
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change the current <see cref="MultiplayerRoom"/> settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A room must be joined for this to have any effect.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="name">The new room name, if any.</param>
|
||||||
|
/// <param name="item">The new room playlist item, if any.</param>
|
||||||
|
public void ChangeSettings(Optional<string> name = default, Optional<PlaylistItem> item = default)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// A dummy playlist item filled with the current room settings (except mods).
|
||||||
|
var existingPlaylistItem = new PlaylistItem
|
||||||
|
{
|
||||||
|
Beatmap =
|
||||||
|
{
|
||||||
|
Value = new BeatmapInfo
|
||||||
|
{
|
||||||
|
OnlineBeatmapID = Room.Settings.BeatmapID,
|
||||||
|
MD5Hash = Room.Settings.BeatmapChecksum
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RulesetID = Room.Settings.RulesetID
|
||||||
|
};
|
||||||
|
|
||||||
|
ChangeSettings(new MultiplayerRoomSettings
|
||||||
|
{
|
||||||
|
Name = name.GetOr(Room.Settings.Name),
|
||||||
|
BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID,
|
||||||
|
BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash,
|
||||||
|
RulesetID = item.GetOr(existingPlaylistItem).RulesetID,
|
||||||
|
Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task TransferHost(int userId);
|
||||||
|
|
||||||
|
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
|
||||||
|
|
||||||
|
public abstract Task ChangeState(MultiplayerUserState newState);
|
||||||
|
|
||||||
|
public abstract Task StartMatch();
|
||||||
|
|
||||||
|
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
Room.State = state;
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case MultiplayerRoomState.Open:
|
||||||
|
apiRoom.Status.Value = new RoomStatusOpen();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerRoomState.Playing:
|
||||||
|
apiRoom.Status.Value = new RoomStatusPlaying();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerRoomState.Closed:
|
||||||
|
apiRoom.Status.Value = new RoomStatusEnded();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
|
||||||
|
{
|
||||||
|
await PopulateUser(user);
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Add(user);
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user)
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Remove(user);
|
||||||
|
PlayingUsers.Remove(user.UserID);
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.HostChanged(int userId)
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
var user = Room.Users.FirstOrDefault(u => u.UserID == userId);
|
||||||
|
|
||||||
|
Room.Host = user;
|
||||||
|
apiRoom.Host.Value = user?.User;
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings)
|
||||||
|
{
|
||||||
|
updateLocalRoomSettings(newSettings);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state)
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Room.Users.Single(u => u.UserID == userId).State = state;
|
||||||
|
|
||||||
|
if (state != MultiplayerUserState.Playing)
|
||||||
|
PlayingUsers.Remove(userId);
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.LoadRequested()
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LoadRequested?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.MatchStarted()
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
var players = Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID).ToList();
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PlayingUsers.AddRange(players);
|
||||||
|
|
||||||
|
MatchStarted?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IMultiplayerClient.ResultsReady()
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ResultsReady?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
|
||||||
|
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the local room settings with the given <see cref="MultiplayerRoomSettings"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This updates both the joined <see cref="MultiplayerRoom"/> and the respective API <see cref="Room"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="settings">The new <see cref="MultiplayerRoomSettings"/> to update from.</param>
|
||||||
|
private void updateLocalRoomSettings(MultiplayerRoomSettings settings)
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Update a few properties of the room instantaneously.
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
Room.Settings = settings;
|
||||||
|
apiRoom.Name.Value = Room.Settings.Name;
|
||||||
|
|
||||||
|
// The playlist update is delayed until an online beatmap lookup (below) succeeds.
|
||||||
|
// In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here.
|
||||||
|
apiRoom.Playlist.Clear();
|
||||||
|
|
||||||
|
RoomChanged?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
|
||||||
|
req.Success += res => updatePlaylist(settings, res);
|
||||||
|
|
||||||
|
api.Queue(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet)
|
||||||
|
{
|
||||||
|
if (Room == null || !Room.Settings.Equals(settings))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(apiRoom != null);
|
||||||
|
|
||||||
|
var beatmapSet = onlineSet.ToBeatmapSet(rulesets);
|
||||||
|
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
|
||||||
|
beatmap.MD5Hash = settings.BeatmapChecksum;
|
||||||
|
|
||||||
|
var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance();
|
||||||
|
var mods = settings.Mods.Select(m => m.ToMod(ruleset));
|
||||||
|
|
||||||
|
PlaylistItem playlistItem = new PlaylistItem
|
||||||
|
{
|
||||||
|
ID = playlistItemId,
|
||||||
|
Beatmap = { Value = beatmap },
|
||||||
|
Ruleset = { Value = ruleset.RulesetInfo },
|
||||||
|
};
|
||||||
|
|
||||||
|
playlistItem.RequiredMods.AddRange(mods);
|
||||||
|
|
||||||
|
apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity.
|
||||||
|
apiRoom.Playlist.Add(playlistItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,9 @@ namespace osu.Game.Screens.Multi.Components
|
|||||||
|
|
||||||
public IBindableList<Room> Rooms => rooms;
|
public IBindableList<Room> Rooms => rooms;
|
||||||
|
|
||||||
|
protected IBindable<Room> JoinedRoom => joinedRoom;
|
||||||
|
private readonly Bindable<Room> joinedRoom = new Bindable<Room>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesets { get; set; }
|
private RulesetStore rulesets { get; set; }
|
||||||
|
|
||||||
@ -37,8 +40,6 @@ namespace osu.Game.Screens.Multi.Components
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider api { get; set; }
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
private Room joinedRoom;
|
|
||||||
|
|
||||||
protected RoomManager()
|
protected RoomManager()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -64,7 +65,7 @@ namespace osu.Game.Screens.Multi.Components
|
|||||||
|
|
||||||
req.Success += result =>
|
req.Success += result =>
|
||||||
{
|
{
|
||||||
joinedRoom = room;
|
joinedRoom.Value = room;
|
||||||
|
|
||||||
update(room, result);
|
update(room, result);
|
||||||
addRoom(room);
|
addRoom(room);
|
||||||
@ -93,7 +94,7 @@ namespace osu.Game.Screens.Multi.Components
|
|||||||
|
|
||||||
currentJoinRoomRequest.Success += () =>
|
currentJoinRoomRequest.Success += () =>
|
||||||
{
|
{
|
||||||
joinedRoom = room;
|
joinedRoom.Value = room;
|
||||||
onSuccess?.Invoke(room);
|
onSuccess?.Invoke(room);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,15 +108,15 @@ namespace osu.Game.Screens.Multi.Components
|
|||||||
api.Queue(currentJoinRoomRequest);
|
api.Queue(currentJoinRoomRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PartRoom()
|
public virtual void PartRoom()
|
||||||
{
|
{
|
||||||
currentJoinRoomRequest?.Cancel();
|
currentJoinRoomRequest?.Cancel();
|
||||||
|
|
||||||
if (joinedRoom == null)
|
if (JoinedRoom == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
api.Queue(new PartRoomRequest(joinedRoom));
|
api.Queue(new PartRoomRequest(joinedRoom.Value));
|
||||||
joinedRoom = null;
|
joinedRoom.Value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly HashSet<int> ignoredRooms = new HashSet<int>();
|
private readonly HashSet<int> ignoredRooms = new HashSet<int>();
|
||||||
@ -124,8 +125,7 @@ namespace osu.Game.Screens.Multi.Components
|
|||||||
{
|
{
|
||||||
if (received == null)
|
if (received == null)
|
||||||
{
|
{
|
||||||
rooms.Clear();
|
ClearRooms();
|
||||||
initialRoomsReceived.Value = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,6 +165,14 @@ namespace osu.Game.Screens.Multi.Components
|
|||||||
initialRoomsReceived.Value = true;
|
initialRoomsReceived.Value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void RemoveRoom(Room room) => rooms.Remove(room);
|
||||||
|
|
||||||
|
protected void ClearRooms()
|
||||||
|
{
|
||||||
|
rooms.Clear();
|
||||||
|
initialRoomsReceived.Value = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a local <see cref="Room"/> with a remote copy.
|
/// Updates a local <see cref="Room"/> with a remote copy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
// 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.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Components;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Multi.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class RealtimeRoomManager : RoomManager
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private StatefulMultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
|
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
|
||||||
|
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
|
||||||
|
private readonly IBindable<bool> isConnected = new Bindable<bool>();
|
||||||
|
private readonly Bindable<bool> allowPolling = new Bindable<bool>();
|
||||||
|
|
||||||
|
private ListingPollingComponent listingPollingComponent;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
isConnected.BindTo(multiplayerClient.IsConnected);
|
||||||
|
isConnected.BindValueChanged(_ => Schedule(updatePolling));
|
||||||
|
JoinedRoom.BindValueChanged(_ => updatePolling());
|
||||||
|
|
||||||
|
updatePolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||||
|
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
|
||||||
|
|
||||||
|
public override void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||||
|
=> base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError);
|
||||||
|
|
||||||
|
public override void PartRoom()
|
||||||
|
{
|
||||||
|
if (JoinedRoom == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var joinedRoom = JoinedRoom.Value;
|
||||||
|
|
||||||
|
base.PartRoom();
|
||||||
|
multiplayerClient.LeaveRoom().Wait();
|
||||||
|
|
||||||
|
// Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case.
|
||||||
|
// This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling.
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
RemoveRoom(joinedRoom);
|
||||||
|
listingPollingComponent.PollImmediately();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void joinMultiplayerRoom(Room room, Action<Room> onSuccess = null)
|
||||||
|
{
|
||||||
|
Debug.Assert(room.RoomID.Value != null);
|
||||||
|
|
||||||
|
var joinTask = multiplayerClient.JoinRoom(room);
|
||||||
|
joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
|
joinTask.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
PartRoom();
|
||||||
|
if (t.Exception != null)
|
||||||
|
Logger.Error(t.Exception, "Failed to join multiplayer room.");
|
||||||
|
}, TaskContinuationOptions.NotOnRanToCompletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePolling()
|
||||||
|
{
|
||||||
|
if (!isConnected.Value)
|
||||||
|
ClearRooms();
|
||||||
|
|
||||||
|
// Don't poll when not connected or when a room has been joined.
|
||||||
|
allowPolling.Value = isConnected.Value && JoinedRoom.Value == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
|
||||||
|
{
|
||||||
|
listingPollingComponent = new RealtimeListingPollingComponent
|
||||||
|
{
|
||||||
|
TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls },
|
||||||
|
AllowPolling = { BindTarget = allowPolling }
|
||||||
|
},
|
||||||
|
new RealtimeSelectionPollingComponent
|
||||||
|
{
|
||||||
|
TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls },
|
||||||
|
AllowPolling = { BindTarget = allowPolling }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private class RealtimeListingPollingComponent : ListingPollingComponent
|
||||||
|
{
|
||||||
|
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AllowPolling.BindValueChanged(allowPolling =>
|
||||||
|
{
|
||||||
|
if (!allowPolling.NewValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsLoaded)
|
||||||
|
PollImmediately();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RealtimeSelectionPollingComponent : SelectionPollingComponent
|
||||||
|
{
|
||||||
|
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
AllowPolling.BindValueChanged(allowPolling =>
|
||||||
|
{
|
||||||
|
if (!allowPolling.NewValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsLoaded)
|
||||||
|
PollImmediately();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
using osu.Game.Screens.Multi.RealtimeMultiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class RealtimeMultiplayerTestScene : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
[Cached(typeof(StatefulMultiplayerClient))]
|
||||||
|
public TestRealtimeMultiplayerClient Client { get; }
|
||||||
|
|
||||||
|
[Cached(typeof(RealtimeRoomManager))]
|
||||||
|
public TestRealtimeRoomManager RoomManager { get; }
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
public Bindable<FilterCriteria> Filter { get; }
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly TestRealtimeRoomContainer content;
|
||||||
|
|
||||||
|
private readonly bool joinRoom;
|
||||||
|
|
||||||
|
public RealtimeMultiplayerTestScene(bool joinRoom = true)
|
||||||
|
{
|
||||||
|
this.joinRoom = joinRoom;
|
||||||
|
base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both });
|
||||||
|
|
||||||
|
Client = content.Client;
|
||||||
|
RoomManager = content.RoomManager;
|
||||||
|
Filter = content.Filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public new void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
RoomManager.Schedule(() => RoomManager.PartRoom());
|
||||||
|
|
||||||
|
if (joinRoom)
|
||||||
|
RoomManager.Schedule(() => RoomManager.CreateRoom(Room));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient
|
||||||
|
{
|
||||||
|
public override IBindable<bool> IsConnected => isConnected;
|
||||||
|
private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; } = null!;
|
||||||
|
|
||||||
|
public void Connect() => isConnected.Value = true;
|
||||||
|
|
||||||
|
public void Disconnect() => isConnected.Value = false;
|
||||||
|
|
||||||
|
public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user });
|
||||||
|
|
||||||
|
public void RemoveUser(User user)
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.User == user));
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (Room.Users.Any())
|
||||||
|
TransferHost(Room.Users.First().UserID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeUserState(int userId, MultiplayerUserState newState)
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
((IMultiplayerClient)this).UserStateChanged(userId, newState);
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
switch (newState)
|
||||||
|
{
|
||||||
|
case MultiplayerUserState.Loaded:
|
||||||
|
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
|
||||||
|
{
|
||||||
|
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
|
||||||
|
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
|
||||||
|
|
||||||
|
((IMultiplayerClient)this).MatchStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.FinishedPlay:
|
||||||
|
if (Room.Users.All(u => u.State != MultiplayerUserState.Playing))
|
||||||
|
{
|
||||||
|
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
|
||||||
|
ChangeUserState(u.UserID, MultiplayerUserState.Results);
|
||||||
|
|
||||||
|
((IMultiplayerClient)this).ResultsReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
|
||||||
|
{
|
||||||
|
var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value };
|
||||||
|
|
||||||
|
var room = new MultiplayerRoom(roomId);
|
||||||
|
room.Users.Add(user);
|
||||||
|
|
||||||
|
if (room.Users.Count == 1)
|
||||||
|
room.Host = user;
|
||||||
|
|
||||||
|
return Task.FromResult(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);
|
||||||
|
|
||||||
|
public override async Task ChangeSettings(MultiplayerRoomSettings settings)
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
await ((IMultiplayerClient)this).SettingsChanged(settings);
|
||||||
|
|
||||||
|
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
||||||
|
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task ChangeState(MultiplayerUserState newState)
|
||||||
|
{
|
||||||
|
ChangeUserState(api.LocalUser.Value.Id, newState);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task StartMatch()
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
||||||
|
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
|
||||||
|
|
||||||
|
return ((IMultiplayerClient)this).LoadRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Online.RealtimeMultiplayer;
|
||||||
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
using osu.Game.Screens.Multi.RealtimeMultiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class TestRealtimeRoomContainer : Container
|
||||||
|
{
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
[Cached(typeof(StatefulMultiplayerClient))]
|
||||||
|
public readonly TestRealtimeMultiplayerClient Client;
|
||||||
|
|
||||||
|
[Cached(typeof(RealtimeRoomManager))]
|
||||||
|
public readonly TestRealtimeRoomManager RoomManager;
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
|
||||||
|
|
||||||
|
public TestRealtimeRoomContainer()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
AddRangeInternal(new Drawable[]
|
||||||
|
{
|
||||||
|
Client = new TestRealtimeMultiplayerClient(),
|
||||||
|
RoomManager = new TestRealtimeRoomManager(),
|
||||||
|
content = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Multi.Lounge.Components;
|
||||||
|
using osu.Game.Screens.Multi.RealtimeMultiplayer;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.RealtimeMultiplayer
|
||||||
|
{
|
||||||
|
public class TestRealtimeRoomManager : RealtimeRoomManager
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuGameBase game { get; set; }
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
|
||||||
|
|
||||||
|
private readonly List<Room> rooms = new List<Room>();
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
int currentScoreId = 0;
|
||||||
|
int currentRoomId = 0;
|
||||||
|
|
||||||
|
((DummyAPIAccess)api).HandleRequest = req =>
|
||||||
|
{
|
||||||
|
switch (req)
|
||||||
|
{
|
||||||
|
case CreateRoomRequest createRoomRequest:
|
||||||
|
var createdRoom = new APICreatedRoom();
|
||||||
|
|
||||||
|
createdRoom.CopyFrom(createRoomRequest.Room);
|
||||||
|
createdRoom.RoomID.Value ??= currentRoomId++;
|
||||||
|
|
||||||
|
rooms.Add(createdRoom);
|
||||||
|
createRoomRequest.TriggerSuccess(createdRoom);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JoinRoomRequest joinRoomRequest:
|
||||||
|
joinRoomRequest.TriggerSuccess();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PartRoomRequest partRoomRequest:
|
||||||
|
partRoomRequest.TriggerSuccess();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GetRoomsRequest getRoomsRequest:
|
||||||
|
var roomsWithoutParticipants = new List<Room>();
|
||||||
|
|
||||||
|
foreach (var r in rooms)
|
||||||
|
{
|
||||||
|
var newRoom = new Room();
|
||||||
|
|
||||||
|
newRoom.CopyFrom(r);
|
||||||
|
newRoom.RecentParticipants.Clear();
|
||||||
|
|
||||||
|
roomsWithoutParticipants.Add(newRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GetRoomRequest getRoomRequest:
|
||||||
|
getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GetBeatmapSetRequest getBeatmapSetRequest:
|
||||||
|
var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type);
|
||||||
|
onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res);
|
||||||
|
onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e);
|
||||||
|
|
||||||
|
// Get the online API from the game's dependencies.
|
||||||
|
game.Dependencies.Get<IAPIProvider>().Queue(onlineReq);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CreateRoomScoreRequest createRoomScoreRequest:
|
||||||
|
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SubmitRoomScoreRequest submitRoomScoreRequest:
|
||||||
|
submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
|
||||||
|
{
|
||||||
|
ID = currentScoreId++,
|
||||||
|
Accuracy = 1,
|
||||||
|
EndedAt = DateTimeOffset.Now,
|
||||||
|
Passed = true,
|
||||||
|
Rank = ScoreRank.S,
|
||||||
|
MaxCombo = 1000,
|
||||||
|
TotalScore = 1000000,
|
||||||
|
User = api.LocalUser.Value,
|
||||||
|
Statistics = new Dictionary<HitResult, int>()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void ClearRooms() => base.ClearRooms();
|
||||||
|
|
||||||
|
public new void Schedule(Action action) => base.Schedule(action);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user